@samitouri / QOS-React-2 / commits / f6fcae58e9

[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);