[Blocks] Add Shell to Fixture (#18803)
* [Blocks] Add Feed page to fixture * Add minimal routing * Always show post with comments
Dan Abramov committed
May 4, 2020 at 01:59 UTC
f6fcae58e9ea651da600b83278608da704e0b774
18 files changed
+500
-125
fixtures/blocks/db.json
+18
-5
@@ -2,18 +2,21 @@
2
"posts": [
3
{
4
"id": 1,
5
- "title": "My First Post",
5
+ "userId": 2,
6
+ "title": "Welcome",
7
"body": "Hello, world!"
8
},
9
{
10
"id": 2,
10
- "title": "My Second Post",
11
+ "userId": 3,
12
+ "title": "A Guide to useEffect",
13
"body": "Let me tell you everything about useEffect"
14
},
15
{
16
"id": 3,
15
- "title": "My Third Post",
16
- "body": "Why is everything so complicated?"
17
+ "userId": 1,
18
+ "title": "Here and There",
19
+ "body": "Browsers are smart"
20
}
21
],
22
"comments": [
@@ -42,5 +45,15 @@
45
"body": "It's still easy",
46
"postId": 3
47
}
45
- ]
48
+ ],
49
+ "users": [{
50
+ "id": 1,
51
+ "name": "Sebastian"
52
+ }, {
53
+ "id": 2,
54
+ "name": "Sophie"
55
+ }, {
56
+ "id": 3,
57
+ "name": "Dan"
58
+ }]
59
}
fixtures/blocks/delay.js
new
+14
@@ -0,0 +1,14 @@
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
+module.exports = (req, res, next) => {
9
+ if (req.query.delay) {
10
+ setTimeout(next, Number(req.query.delay));
11
+ } else {
12
+ next();
13
+ }
14
+};
fixtures/blocks/package.json
+2
-1
@@ -2,6 +2,7 @@
2
"name": "blocks",
3
"version": "0.1.0",
4
"private": true,
5
+ "proxy": "http://localhost:3001/",
6
"dependencies": {
7
"concurrently": "^5.2.0",
8
"json-server": "^0.16.1",
@@ -13,7 +14,7 @@
14
"prestart": "cp -r ../../build/node_modules/* ./node_modules/",
15
"prebuild": "cp -r ../../build/node_modules/* ./node_modules/",
16
"start": "concurrently \"npm run start:client\" \"npm run start:api\"",
16
- "start:api": "json-server --watch db.json --port 3001 --delay 300",
17
+ "start:api": "json-server --watch db.json --port 3001 --delay 1000 --middlewares delay.js",
18
"start:client": "react-scripts start",
19
"build": "react-scripts build",
20
"eject": "react-scripts eject"
fixtures/blocks/src/App.js
deleted
-73
@@ -1,73 +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
-
8
-import React, {useReducer, useTransition, Suspense} from 'react';
9
-import loadPost from './Post';
10
-import {createCache, CacheProvider} from 'react/unstable-cache';
11
-
12
-const initialState = {
13
- // TODO: use this for invalidation.
14
- cache: createCache(),
15
- params: {id: 1},
16
- RootBlock: loadPost({id: 1}),
17
-};
18
-
19
-function reducer(state, action) {
20
- switch (action.type) {
21
- case 'navigate':
22
- // TODO: cancel previous fetch?
23
- return {
24
- cache: state.cache,
25
- params: action.nextParams,
26
- RootBlock: loadPost(action.nextParams),
27
- };
28
- default:
29
- throw new Error();
30
- }
31
-}
32
-
33
-function Router() {
34
- const [state, dispatch] = useReducer(reducer, initialState);
35
- const [startTransition, isPending] = useTransition({
36
- timeoutMs: 3000,
37
- });
38
- return (
39
- <>
40
- <button
41
- disabled={isPending}
42
- onClick={() => {
43
- startTransition(() => {
44
- dispatch({
45
- type: 'navigate',
46
- nextParams: {
47
- id: state.params.id === 3 ? 1 : state.params.id + 1,
48
- },
49
- });
50
- });
51
- }}>
52
- Next
53
- </button>
54
- {isPending && ' ...'}
55
- <hr />
56
- <Suspense fallback={<h4>Loading Page...</h4>}>
57
- <CacheProvider value={state.cache}>
58
- <state.RootBlock />
59
- </CacheProvider>
60
- </Suspense>
61
- </>
62
- );
63
-}
64
-
65
-function Root() {
66
- return (
67
- <Suspense fallback={<h1>Loading App...</h1>}>
68
- <Router />
69
- </Suspense>
70
- );
71
-}
72
-
73
-export default Root;
fixtures/blocks/src/Post.js
deleted
-41
@@ -1,41 +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
-
8
-import * as React from 'react';
9
-import {block, Suspense} from 'react';
10
-import {fetch, preload} from 'react-data/fetch';
11
-import loadComments from './Comments';
12
-
13
-function load(params) {
14
- if (params.id < 3) {
15
- const nextId = params.id + 1;
16
- preload('http://localhost:3001/posts/' + nextId);
17
- }
18
- const postResponse = fetch('http://localhost:3001/posts/' + params.id);
19
- return {
20
- post: postResponse.json(),
21
- meta: postResponse.status + ' ' + postResponse.statusText,
22
- Comments: loadComments(params.id),
23
- };
24
-}
25
-
26
-function Post(props, data) {
27
- return (
28
- <>
29
- <h1>Post {data.post.id}</h1>
30
- <h4>{data.post.title}</h4>
31
- <p>{data.post.body}</p>
32
- <pre>{data.meta}</pre>
33
- <hr />
34
- <Suspense fallback={<p>Loading comments...</p>}>
35
- <data.Comments />
36
- </Suspense>
37
- </>
38
- );
39
-}
40
-
41
-export default block(Post, load);
fixtures/blocks/src/Router.js
new
+96
@@ -0,0 +1,96 @@
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 React, {
9
+ useReducer,
10
+ useEffect,
11
+ useTransition,
12
+ useCallback,
13
+ useMemo,
14
+ Suspense,
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';
20
+
21
+const initialUrl = window.location.pathname;
22
+
23
+const initialState = {
24
+ // TODO: use this for invalidation.
25
+ cache: createCache(),
26
+ url: initialUrl,
27
+ RootBlock: loadApp(initialUrl),
28
+};
29
+
30
+function reducer(state, action) {
31
+ switch (action.type) {
32
+ case 'navigate':
33
+ // TODO: cancel previous fetch?
34
+ return {
35
+ cache: state.cache,
36
+ url: action.url,
37
+ RootBlock: loadApp(action.url),
38
+ };
39
+ default:
40
+ throw new Error();
41
+ }
42
+}
43
+
44
+function Router() {
45
+ const [state, dispatch] = useReducer(reducer, initialState);
46
+ const [startTransition, isPending] = useTransition({
47
+ timeoutMs: 3000,
48
+ });
49
+
50
+ useEffect(() => {
51
+ document.body.style.cursor = isPending ? 'wait' : '';
52
+ }, [isPending]);
53
+
54
+ const navigate = useCallback(
55
+ url => {
56
+ startTransition(() => {
57
+ // TODO: Here, There, and Everywhere.
58
+ // TODO: Instant Transitions, somehow.
59
+ // TODO: Buttons should update immediately.
60
+ dispatch({
61
+ type: 'navigate',
62
+ url,
63
+ });
64
+ });
65
+ },
66
+ [startTransition]
67
+ );
68
+
69
+ useEffect(() => {
70
+ const listener = () => {
71
+ navigate(window.location.pathname);
72
+ };
73
+ window.addEventListener('popstate', listener);
74
+ return () => window.removeEventListener('popstate', listener);
75
+ }, [navigate]);
76
+
77
+ const routeContext = useMemo(
78
+ () => ({
79
+ url: state.url,
80
+ navigate,
81
+ }),
82
+ [state.url, navigate]
83
+ );
84
+
85
+ return (
86
+ <Suspense fallback={<h2>Loading...</h2>}>
87
+ <CacheProvider value={state.cache}>
88
+ <RouterProvider value={routeContext}>
89
+ <state.RootBlock />
90
+ </RouterProvider>
91
+ </CacheProvider>
92
+ </Suspense>
93
+ );
94
+}
95
+
96
+export default Router;
fixtures/blocks/src/client/RouterContext.js
new
+9
@@ -0,0 +1,9 @@
1
+import {createContext, useContext} from 'react';
2
+
3
+const RouterContext = createContext(null);
4
+
5
+export const RouterProvider = RouterContext.Provider;
6
+
7
+export function useRouter() {
8
+ return useContext(RouterContext);
9
+}
fixtures/blocks/src/client/Shell.js
new
+63
@@ -0,0 +1,63 @@
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 {useRouter} from './RouterContext';
10
+
11
+function TabBar({children}) {
12
+ return (
13
+ <div style={{border: '1px solid #aaa', padding: 20, width: 500}}>
14
+ {children}
15
+ </div>
16
+ );
17
+}
18
+
19
+function TabLink({to, children}) {
20
+ const {url: activeUrl, navigate} = useRouter();
21
+ const active = activeUrl === to;
22
+ if (active) {
23
+ return (
24
+ <b
25
+ style={{
26
+ display: 'inline-block',
27
+ width: 50,
28
+ marginRight: 20,
29
+ }}>
30
+ {children}
31
+ </b>
32
+ );
33
+ }
34
+ return (
35
+ <a
36
+ style={{
37
+ display: 'inline-block',
38
+ width: 50,
39
+ marginRight: 20,
40
+ }}
41
+ href={to}
42
+ onClick={e => {
43
+ e.preventDefault();
44
+ window.history.pushState(null, null, to);
45
+ navigate(to);
46
+ }}>
47
+ {children}
48
+ </a>
49
+ );
50
+}
51
+
52
+export default function Shell({children}) {
53
+ return (
54
+ <>
55
+ <TabBar>
56
+ <TabLink to="/">Home</TabLink>
57
+ <TabLink to="/profile">Profile</TabLink>
58
+ </TabBar>
59
+ <br />
60
+ {children}
61
+ </>
62
+ );
63
+}
fixtures/blocks/src/index.css
+4
@@ -2,3 +2,7 @@ body {
2
font-family: Helvetica;
3
padding-left: 10px;
4
}
5
+
6
+* {
7
+ box-sizing: border-box;
8
+}
fixtures/blocks/src/index.js
+2
-2
@@ -8,6 +8,6 @@
8
import React from 'react';
9
import ReactDOM from 'react-dom';
10
import './index.css';
11
-import App from './App';
11
+import Router from './Router';
12
13
-ReactDOM.createRoot(document.getElementById('root')).render(<App />);
13
+ReactDOM.createRoot(document.getElementById('root')).render(<Router />);
fixtures/blocks/src/server/App.block.js
new
+44
@@ -0,0 +1,44 @@
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 {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
+ switch (url) {
20
+ case '/':
21
+ Page = loadFeedPage();
22
+ break;
23
+ case '/profile':
24
+ Page = loadProfilePage(3);
25
+ break;
26
+ default:
27
+ throw Error('Not found');
28
+ }
29
+ return {Page};
30
+}
31
+
32
+// Client
33
+
34
+import Shell from '../client/Shell';
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/Comments.block.js
renamed
+10
-3
@@ -4,20 +4,27 @@
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 {block} from 'react';
11
+
12
+// Server
13
+
14
import {fetch} from 'react-data/fetch';
15
16
function load(postId) {
17
return {
13
- comments: fetch('http://localhost:3001/comments?postId=' + postId).json(),
18
+ comments: fetch(`/comments?postId=${postId}`).json(),
19
};
20
}
21
22
+// Client
23
+
24
function Comments(props, data) {
25
return (
26
<>
20
- <h3>Comments</h3>
27
+ <h5>Comments</h5>
28
<ul>
29
{data.comments.slice(0, 5).map(item => (
30
<li key={item.id}>{item.body}</li>
@@ -27,4 +34,4 @@ function Comments(props, data) {
34
);
35
}
36
30
-export default React.block(Comments, load);
37
+export default block(Comments, load);
fixtures/blocks/src/server/FeedPage.block.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
+/* eslint-disable import/first */
8
+
9
+import * as React from 'react';
10
+import {block} from 'react';
11
+import {fetch} from 'react-data/fetch';
12
+
13
+// Server
14
+
15
+import PostList from './PostList';
16
+
17
+function load(params) {
18
+ const allPosts = fetch('/posts').json();
19
+ return {
20
+ posts: <PostList posts={allPosts} />,
21
+ };
22
+}
23
+
24
+// Client
25
+
26
+function FeedPage(props, data) {
27
+ return (
28
+ <>
29
+ <h2>Feed</h2>
30
+ {data.posts}
31
+ </>
32
+ );
33
+}
34
+
35
+export default block(FeedPage, load);
fixtures/blocks/src/server/Post.block.js
new
+45
@@ -0,0 +1,45 @@
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 {block, Suspense} from 'react';
11
+
12
+// Server
13
+
14
+import loadComments from './Comments.block';
15
+
16
+function load(postId) {
17
+ return {
18
+ Comments: loadComments(postId),
19
+ };
20
+}
21
+
22
+// Client
23
+
24
+function Post(props, data) {
25
+ return (
26
+ <div
27
+ style={{
28
+ border: '1px solid #aaa',
29
+ borderRadius: 10,
30
+ marginBottom: 20,
31
+ padding: 20,
32
+ maxWidth: 500,
33
+ }}>
34
+ <h4 style={{marginTop: 0}}>{props.post.title}</h4>
35
+ <p>{props.post.body}</p>
36
+ <Suspense
37
+ fallback={<h5>Loading comments...</h5>}
38
+ unstable_avoidThisFallback={true}>
39
+ <data.Comments />
40
+ </Suspense>
41
+ </div>
42
+ );
43
+}
44
+
45
+export default block(Post, load);
fixtures/blocks/src/server/PostGlimmer.js
new
+56
@@ -0,0 +1,56 @@
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
+export default function PostGlimmer() {
12
+ return (
13
+ <div
14
+ style={{
15
+ border: '1px solid #aaa',
16
+ borderRadius: 10,
17
+ marginBottom: 20,
18
+ padding: 20,
19
+ maxWidth: 500,
20
+ height: 180,
21
+ }}>
22
+ <div
23
+ style={{
24
+ marginBottom: 20,
25
+ width: '50%',
26
+ height: 20,
27
+ background: '#ddd',
28
+ }}
29
+ />
30
+ <div
31
+ style={{
32
+ marginBottom: 20,
33
+ width: '60%',
34
+ height: 20,
35
+ background: '#eee',
36
+ }}
37
+ />
38
+ <div
39
+ style={{
40
+ marginBottom: 20,
41
+ width: '50%',
42
+ height: 20,
43
+ background: '#eee',
44
+ }}
45
+ />
46
+ <div
47
+ style={{
48
+ marginBottom: 20,
49
+ width: '60%',
50
+ height: 20,
51
+ background: '#eee',
52
+ }}
53
+ />
54
+ </div>
55
+ );
56
+}
fixtures/blocks/src/server/PostList.js
new
+30
@@ -0,0 +1,30 @@
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, SuspenseList} from 'react';
11
+import {preload} from 'react-data/fetch';
12
+
13
+import loadPost from './Post.block';
14
+import PostGlimmer from './PostGlimmer';
15
+
16
+export default function PostList({posts}) {
17
+ return (
18
+ <SuspenseList revealOrder="forwards" tail="collapsed">
19
+ {posts.map(post => {
20
+ preload(`/comments?postId=${post.id}`);
21
+ const Post = loadPost(post.id);
22
+ return (
23
+ <Suspense key={post.id} fallback={<PostGlimmer />}>
24
+ <Post post={post} />
25
+ </Suspense>
26
+ );
27
+ })}
28
+ </SuspenseList>
29
+ );
30
+}
fixtures/blocks/src/server/ProfilePage.block.js
new
+37
@@ -0,0 +1,37 @@
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 {block, Suspense} from 'react';
11
+
12
+// Server
13
+
14
+import {fetch} from 'react-data/fetch';
15
+import loadProfileTimeline from './ProfileTimeline.block';
16
+
17
+function load(userId) {
18
+ return {
19
+ user: fetch(`/users/${userId}`).json(),
20
+ ProfileTimeline: loadProfileTimeline(userId),
21
+ };
22
+}
23
+
24
+// Client
25
+
26
+function ProfilePage(props, data) {
27
+ return (
28
+ <>
29
+ <h2>{data.user.name}</h2>
30
+ <Suspense fallback={<h3>Loading Timeline...</h3>}>
31
+ <data.ProfileTimeline />
32
+ </Suspense>
33
+ </>
34
+ );
35
+}
36
+
37
+export default block(ProfilePage, load);
fixtures/blocks/src/server/ProfileTimeline.block.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
+/* eslint-disable import/first */
8
+
9
+import * as React from 'react';
10
+import {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}`).json();
19
+ return {
20
+ posts: <PostList posts={postsByUser} />,
21
+ };
22
+}
23
+
24
+// Client
25
+
26
+function ProfileTimeline(props, data) {
27
+ return (
28
+ <>
29
+ <h3>Timeline</h3>
30
+ {data.posts}
31
+ </>
32
+ );
33
+}
34
+
35
+export default block(ProfileTimeline, load);