[Blocks Fixture] Drop the Blocks (#18837)
* [Blocks Fixture] Add a tiny router * Add a way to load nested entrypoint * Only expose URL params to nested routers * Add keys to route definitions * [Blocks Fixture] Drop the Blocks
Dan Abramov committed
May 6, 2020 at 05:17 UTC
595d27bd73f0242052a8e0cb3ac7400e0ec58d12
18 files changed
+272
-222
fixtures/blocks/src/Router.js
+6
-9
@@ -15,17 +15,16 @@ import React, {
15
} from 'react';
16
import {createCache, CacheProvider} from 'react/unstable-cache';
17
import {RouterProvider} from './client/RouterContext';
18
-// TODO: entry point. This can't really done in the client code.
19
-import loadApp from './server/App.block';
18
+// TODO: can't really import a server component on the client.
19
+import App from './server/App';
20
21
const initialUrl = window.location.pathname;
22
-
22
const initialState = {
23
// TODO: use this for invalidation.
24
cache: createCache(),
25
url: initialUrl,
26
pendingUrl: initialUrl,
28
- RootBlock: loadApp(initialUrl),
27
+ root: <App route={initialUrl} />,
28
};
29
30
function reducer(state, action) {
@@ -41,7 +40,7 @@ function reducer(state, action) {
40
...state,
41
url: action.url,
42
pendingUrl: action.url,
44
- RootBlock: action.RootBlock,
43
+ root: action.root,
44
};
45
default:
46
throw new Error();
@@ -65,7 +64,7 @@ function Router() {
64
// TODO: Instant Transitions, somehow.
65
dispatch({
66
type: 'completeNavigation',
68
- RootBlock: loadApp(url),
67
+ root: <App route={url} />,
68
url,
69
});
70
});
@@ -97,9 +96,7 @@ function Router() {
96
return (
97
<Suspense fallback={<h2>Loading...</h2>}>
98
<CacheProvider value={state.cache}>
100
- <RouterProvider value={routeContext}>
101
- <state.RootBlock />
102
- </RouterProvider>
99
+ <RouterProvider value={routeContext}>{state.root}</RouterProvider>
100
</CacheProvider>
101
</Suspense>
102
);
fixtures/blocks/src/client/ProfileNav.js
new
+21
@@ -0,0 +1,21 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+/* eslint-disable import/first */
8
+
9
+import * as React from 'react';
10
+
11
+import {TabBar, TabLink} from '../client/TabNav';
12
+
13
+export default function ProfileNav({userId}) {
14
+ // TODO: Don't hardcode ID.
15
+ return (
16
+ <TabBar>
17
+ <TabLink to={`/profile/${userId}`}>Timeline</TabLink>
18
+ <TabLink to={`/profile/${userId}/bio`}>Bio</TabLink>
19
+ </TabBar>
20
+ );
21
+}
fixtures/blocks/src/server/App.block.js
deleted
-44
@@ -1,44 +0,0 @@
1
-/**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- */
7
-/* eslint-disable import/first */
8
-
9
-import * as React from 'react';
10
-import {unstable_block as block} from 'react';
11
-
12
-// Server
13
-
14
-import loadFeedPage from './FeedPage.block';
15
-import loadProfilePage from './ProfilePage.block';
16
-
17
-function load(url) {
18
- let Page;
19
- const segments = url.split('/').filter(Boolean);
20
- if (segments.length === 0) {
21
- Page = loadFeedPage();
22
- } else if (segments[0] === 'profile') {
23
- Page = loadProfilePage(Number(segments[1]), segments[2]);
24
- } else {
25
- throw Error('Not found');
26
- }
27
- return {Page};
28
-}
29
-
30
-// Client
31
-
32
-import Shell from '../client/Shell';
33
-
34
-// TODO: some notion of key.
35
-
36
-function App(props, data) {
37
- return (
38
- <Shell>
39
- <data.Page />
40
- </Shell>
41
- );
42
-}
43
-
44
-export default block(App, load);
fixtures/blocks/src/server/App.js
new
+29
@@ -0,0 +1,29 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+/* eslint-disable import/first */
8
+
9
+import * as React from 'react';
10
+import {matchRoute} from './ServerRouter';
11
+import FeedPage from './FeedPage';
12
+import ProfilePage from './ProfilePage';
13
+
14
+// TODO: Replace with asset reference.
15
+import loadShell from './Shell.block';
16
+
17
+// TODO: Router component?
18
+const AppRoutes = {
19
+ '/': props => <FeedPage {...props} key="home" />,
20
+ '/profile/:userId/*': props => (
21
+ <ProfilePage {...props} key={`profile-${props.userId}`} />
22
+ ),
23
+};
24
+
25
+export default function App(props) {
26
+ const Shell = loadShell();
27
+ const match = matchRoute(props, AppRoutes);
28
+ return <Shell>{match}</Shell>;
29
+}
fixtures/blocks/src/server/Comments.js
renamed
+6
-17
@@ -7,28 +7,19 @@
7
/* eslint-disable import/first */
8
9
import * as React from 'react';
10
-import {unstable_block as block} from 'react';
11
-
12
-// Server
13
-
10
import {fetch} from 'react-data/fetch';
11
16
-function load(postId) {
17
- return {
18
- comments: fetch(`/comments?postId=${postId}&_expand=user`).json(),
19
- };
20
-}
21
-
22
-// Client
23
-
24
-import Link from '../client/Link';
12
+// TODO: Replace with asset reference.
13
+import loadLink from './Link.block';
14
26
-function Comments(props, data) {
15
+export default function Comments({postId}) {
16
+ const Link = loadLink();
17
+ const comments = fetch(`/comments?postId=${postId}&_expand=user`).json();
18
return (
19
<>
20
<h5>Comments</h5>
21
<ul>
31
- {data.comments.slice(0, 5).map(comment => (
22
+ {comments.slice(0, 5).map(comment => (
23
<li key={comment.id}>
24
{comment.body}
25
{' • '}
@@ -39,5 +30,3 @@ function Comments(props, data) {
30
</>
31
);
32
}
42
-
43
-export default block(Comments, load);
fixtures/blocks/src/server/Feed.js
renamed
+3
-17
@@ -7,29 +7,15 @@
7
/* eslint-disable import/first */
8
9
import * as React from 'react';
10
-import {unstable_block as block} from 'react';
10
import {fetch} from 'react-data/fetch';
12
-
13
-// Server
14
-
11
import PostList from './PostList';
12
17
-function load(params) {
18
- const allPosts = fetch('/posts?_expand=user').json();
19
- return {
20
- posts: <PostList posts={allPosts} />,
21
- };
22
-}
23
-
24
-// Client
25
-
26
-function FeedPage(props, data) {
13
+export default function Feed() {
14
+ const posts = fetch('/posts?_expand=user').json();
15
return (
16
<>
17
<h2>Feed</h2>
30
- {data.posts}
18
+ <PostList posts={posts} />
19
</>
20
);
21
}
34
-
35
-export default block(FeedPage, load);
fixtures/blocks/src/server/FeedPage.js
new
+20
@@ -0,0 +1,20 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+/* eslint-disable import/first */
8
+
9
+import * as React from 'react';
10
+import {Suspense} from 'react';
11
+import PostGlimmer from './PostGlimmer';
12
+import Feed from './Feed';
13
+
14
+export default function FeedPage() {
15
+ return (
16
+ <Suspense fallback={<PostGlimmer />}>
17
+ <Feed />
18
+ </Suspense>
19
+ );
20
+}
fixtures/blocks/src/server/Link.block.js
new
+22
@@ -0,0 +1,22 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+/* eslint-disable import/first */
8
+
9
+import * as React from 'react';
10
+import {unstable_block as block} from 'react';
11
+
12
+// Client
13
+
14
+// TODO: delete this wrapper.
15
+
16
+import ClientLink from '../client/Link';
17
+
18
+function Link(props, _) {
19
+ return <ClientLink {...props} />;
20
+}
21
+
22
+export default block(Link);
fixtures/blocks/src/server/Post.js
renamed
+10
-23
@@ -7,23 +7,14 @@
7
/* eslint-disable import/first */
8
9
import * as React from 'react';
10
-import {unstable_block as block, Suspense} from 'react';
10
+import {Suspense} from 'react';
11
+import Comments from './Comments';
12
12
-// Server
13
+// TODO: Replace with asset reference.
14
+import loadLink from './Link.block';
15
14
-import loadComments from './Comments.block';
15
-
16
-function load(postId) {
17
- return {
18
- Comments: loadComments(postId),
19
- };
20
-}
21
-
22
-// Client
23
-
24
-import Link from '../client/Link';
25
-
26
-function Post(props, data) {
16
+export default function Post({post}) {
17
+ const Link = loadLink();
18
return (
19
<div
20
style={{
@@ -34,20 +25,16 @@ function Post(props, data) {
25
maxWidth: 500,
26
}}>
27
<h4 style={{marginTop: 0}}>
37
- {props.post.title}
28
+ {post.title}
29
{' by '}
39
- <Link to={`/profile/${props.post.user.id}`}>
40
- {props.post.user.name}
41
- </Link>
30
+ <Link to={`/profile/${post.user.id}`}>{post.user.name}</Link>
31
</h4>
43
- <p>{props.post.body}</p>
32
+ <p>{post.body}</p>
33
<Suspense
34
fallback={<h5>Loading comments...</h5>}
35
unstable_avoidThisFallback={true}>
47
- <data.Comments />
36
+ <Comments postId={post.id} />
37
</Suspense>
38
</div>
39
);
40
}
52
-
53
-export default block(Post, load);
fixtures/blocks/src/server/PostList.js
+1
-3
@@ -9,16 +9,14 @@
9
import * as React from 'react';
10
import {Suspense, unstable_SuspenseList as SuspenseList} from 'react';
11
import {preload} from 'react-data/fetch';
12
-
13
-import loadPost from './Post.block';
12
import PostGlimmer from './PostGlimmer';
13
+import Post from './Post';
14
15
export default function PostList({posts}) {
16
return (
17
<SuspenseList revealOrder="forwards" tail="collapsed">
18
{posts.map(post => {
19
preload(`/comments?postId=${post.id}&_expand=user`);
21
- const Post = loadPost(post.id);
20
return (
21
<Suspense key={post.id} fallback={<PostGlimmer />}>
22
<Post post={post} />
fixtures/blocks/src/server/ProfileBio.js
renamed
+5
-18
@@ -7,28 +7,15 @@
7
/* eslint-disable import/first */
8
9
import * as React from 'react';
10
-import {unstable_block as block} from 'react';
11
-
12
-// Server
13
-
10
import {fetch} from 'react-data/fetch';
11
16
-function load(user) {
17
- return {
18
- user,
19
- bio: fetch(`/bios/${user.bioId}`).json(),
20
- };
21
-}
22
-
23
-// Client
24
-
25
-function ProfileBio(props, data) {
12
+export default function ProfileBio({userId}) {
13
+ const user = fetch(`/users/${userId}`).json();
14
+ const bio = fetch(`/bios/${user.bioId}`).json().text;
15
return (
16
<>
28
- <h3>{data.user.name}'s Bio</h3>
29
- <p>{data.bio.text}</p>
17
+ <h3>{user.name}'s Bio</h3>
18
+ <p>{bio}</p>
19
</>
20
);
21
}
33
-
34
-export default block(ProfileBio, load);
fixtures/blocks/src/server/ProfileNav.block.js
new
+22
@@ -0,0 +1,22 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+/* eslint-disable import/first */
8
+
9
+import * as React from 'react';
10
+import {unstable_block as block} from 'react';
11
+
12
+// Client
13
+
14
+// TODO: delete this wrapper.
15
+
16
+import ClientProfileNav from '../client/ProfileNav';
17
+
18
+function ProfileNav(props, _) {
19
+ return <ClientProfileNav {...props} />;
20
+}
21
+
22
+export default block(ProfileNav);
fixtures/blocks/src/server/ProfilePage.block.js
deleted
-61
@@ -1,61 +0,0 @@
1
-/**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- */
7
-/* eslint-disable import/first */
8
-
9
-import * as React from 'react';
10
-import {unstable_block as block, Suspense} from 'react';
11
-
12
-// Server
13
-
14
-import {fetch} from 'react-data/fetch';
15
-import loadProfileBio from './ProfileBio.block';
16
-import loadProfileTimeline from './ProfileTimeline.block';
17
-
18
-function load(userId, tab) {
19
- const user = fetch(`/users/${userId}`).json();
20
- let Tab;
21
- switch (tab) {
22
- case 'bio':
23
- Tab = loadProfileBio(user);
24
- break;
25
- default:
26
- Tab = loadProfileTimeline(userId);
27
- break;
28
- }
29
- return {
30
- Tab,
31
- user,
32
- };
33
-}
34
-
35
-// Client
36
-
37
-import {TabBar, TabLink} from '../client/TabNav';
38
-
39
-function ProfileTabNav({userId}) {
40
- // TODO: Don't hardcode ID.
41
- return (
42
- <TabBar>
43
- <TabLink to={`/profile/${userId}`}>Timeline</TabLink>
44
- <TabLink to={`/profile/${userId}/bio`}>Bio</TabLink>
45
- </TabBar>
46
- );
47
-}
48
-
49
-function ProfilePage(props, data) {
50
- return (
51
- <>
52
- <h2>{data.user.name}</h2>
53
- <ProfileTabNav userId={data.user.id} />
54
- <Suspense fallback={<h3>Loading...</h3>}>
55
- <data.Tab />
56
- </Suspense>
57
- </>
58
- );
59
-}
60
-
61
-export default block(ProfilePage, load);
fixtures/blocks/src/server/ProfilePage.js
new
+35
@@ -0,0 +1,35 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import * as React from 'react';
9
+import {Suspense} from 'react';
10
+import {fetch} from 'react-data/fetch';
11
+import {matchRoute} from './ServerRouter';
12
+import ProfileTimeline from './ProfileTimeline';
13
+import ProfileBio from './ProfileBio';
14
+
15
+// TODO: Replace with asset reference.
16
+import loadProfileNav from './ProfileNav.block';
17
+
18
+// TODO: Router component?
19
+const ProfileRoutes = {
20
+ '/': props => <ProfileTimeline {...props} key="timeline" />,
21
+ '/bio': props => <ProfileBio {...props} key="bio" />,
22
+};
23
+
24
+export default function ProfilePage(props) {
25
+ const user = fetch(`/users/${props.userId}`).json();
26
+ const ProfileNav = loadProfileNav();
27
+ const match = matchRoute(props, ProfileRoutes);
28
+ return (
29
+ <>
30
+ <h2>{user.name}</h2>
31
+ <ProfileNav userId={user.id} />
32
+ <Suspense fallback={<h3>Loading...</h3>}>{match}</Suspense>
33
+ </>
34
+ );
35
+}
fixtures/blocks/src/server/ProfileTimeline.block.js
deleted
-30
@@ -1,30 +0,0 @@
1
-/**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- */
7
-/* eslint-disable import/first */
8
-
9
-import * as React from 'react';
10
-import {unstable_block as block} from 'react';
11
-
12
-// Server
13
-
14
-import {fetch} from 'react-data/fetch';
15
-import PostList from './PostList';
16
-
17
-function load(userId) {
18
- const postsByUser = fetch(`/posts?userId=${userId}&_expand=user`).json();
19
- return {
20
- posts: <PostList posts={postsByUser} />,
21
- };
22
-}
23
-
24
-// Client
25
-
26
-function ProfileTimeline(props, data) {
27
- return <>{data.posts}</>;
28
-}
29
-
30
-export default block(ProfileTimeline, load);
fixtures/blocks/src/server/ProfileTimeline.js
new
+16
@@ -0,0 +1,16 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+/* eslint-disable import/first */
8
+
9
+import * as React from 'react';
10
+import {fetch} from 'react-data/fetch';
11
+import PostList from './PostList';
12
+
13
+export default function ProfileTimeline({userId}) {
14
+ const posts = fetch(`/posts?userId=${userId}&_expand=user`).json();
15
+ return <PostList posts={posts} />;
16
+}
fixtures/blocks/src/server/ServerRouter.js
new
+54
@@ -0,0 +1,54 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+/* eslint-disable import/first */
8
+
9
+function tryMatch(props, def) {
10
+ const defSegments = def.split('/').filter(Boolean);
11
+ const routeSegments = props.route.split('/').filter(Boolean);
12
+ let innerProps = {...props};
13
+ while (routeSegments.length > 0) {
14
+ if (defSegments.length === 0) {
15
+ return null;
16
+ }
17
+ const urlSegment = routeSegments.shift();
18
+ const defSegment = defSegments.shift();
19
+ if (urlSegment === defSegment) {
20
+ continue;
21
+ }
22
+ if (defSegment[0] === ':') {
23
+ innerProps[defSegment.slice(1)] = urlSegment;
24
+ continue;
25
+ }
26
+ if (defSegment === '*') {
27
+ innerProps.route = '/' + urlSegment + routeSegments.join('/');
28
+ return innerProps;
29
+ }
30
+ return null;
31
+ }
32
+ if (defSegments.length === 0) {
33
+ return innerProps;
34
+ }
35
+ if (defSegments.length === 1 && defSegments[0] === '*') {
36
+ innerProps.route = '/';
37
+ return innerProps;
38
+ }
39
+ return null;
40
+}
41
+
42
+export function matchRoute(props, defs) {
43
+ for (let def in defs) {
44
+ if (!defs.hasOwnProperty(def)) {
45
+ continue;
46
+ }
47
+ const innerProps = tryMatch(props, def);
48
+ if (innerProps) {
49
+ const match = defs[def](innerProps);
50
+ return match;
51
+ }
52
+ }
53
+ throw Error('Not found.');
54
+}
fixtures/blocks/src/server/Shell.block.js
new
+22
@@ -0,0 +1,22 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+/* eslint-disable import/first */
8
+
9
+import * as React from 'react';
10
+import {unstable_block as block} from 'react';
11
+
12
+// Client
13
+
14
+// TODO: delete this wrapper.
15
+
16
+import ClientShell from '../client/Shell';
17
+
18
+function Shell(props, _) {
19
+ return <ClientShell {...props} />;
20
+}
21
+
22
+export default block(Shell);