[Blocks Fixture] Misc updates (#18811)
* [Blocks Fixture] Update navigation buttons immediately * Add more profile links * Minor refactor * Add subroutes to Profile
Dan Abramov committed
May 5, 2020 at 11:53 UTC
4d124a4f67d9db6c6e97beab0dd8d7ae8a9177dd
13 files changed
+224
-84
fixtures/blocks/db.json
+26
-8
@@ -23,37 +23,55 @@
23
{
24
"id": 1,
25
"body": "Hey there",
26
- "postId": 1
26
+ "postId": 1,
27
+ "userId": 1
28
},
29
{
30
"id": 2,
31
"body": "Welcome to the chat",
31
- "postId": 1
32
+ "postId": 1,
33
+ "userId": 2
34
},
35
{
36
"id": 3,
37
"body": "What editor/font are you using?",
36
- "postId": 2
38
+ "postId": 2,
39
+ "userId": 2
40
},
41
{
42
"id": 4,
43
"body": "It's always been hard",
41
- "postId": 3
44
+ "postId": 3,
45
+ "userId": 1
46
},
47
{
48
"id": 5,
49
"body": "It's still easy",
46
- "postId": 3
50
+ "postId": 3,
51
+ "userId": 2
52
}
53
],
54
"users": [{
55
"id": 1,
51
- "name": "Sebastian"
56
+ "name": "Sebastian",
57
+ "bioId": 10
58
}, {
59
"id": 2,
54
- "name": "Sophie"
60
+ "name": "Sophie",
61
+ "bioId": 20
62
}, {
63
"id": 3,
57
- "name": "Dan"
64
+ "name": "Dan",
65
+ "bioId": 30
66
+ }],
67
+ "bios": [{
68
+ "id": 10,
69
+ "text": "I like European movies"
70
+ }, {
71
+ "id": 20,
72
+ "text": "I like math puzzles"
73
+ }, {
74
+ "id": 30,
75
+ "text": "I like reading twitter"
76
}]
77
}
fixtures/blocks/src/Router.js
+19
-7
@@ -24,17 +24,24 @@ const initialState = {
24
// TODO: use this for invalidation.
25
cache: createCache(),
26
url: initialUrl,
27
+ pendingUrl: initialUrl,
28
RootBlock: loadApp(initialUrl),
29
};
30
31
function reducer(state, action) {
32
switch (action.type) {
32
- case 'navigate':
33
+ case 'startNavigation':
34
+ return {
35
+ ...state,
36
+ pendingUrl: action.url,
37
+ };
38
+ case 'completeNavigation':
39
// TODO: cancel previous fetch?
40
return {
35
- cache: state.cache,
41
+ ...state,
42
url: action.url,
37
- RootBlock: loadApp(action.url),
43
+ pendingUrl: action.url,
44
+ RootBlock: action.RootBlock,
45
};
46
default:
47
throw new Error();
@@ -44,7 +51,7 @@ function reducer(state, action) {
51
function Router() {
52
const [state, dispatch] = useReducer(reducer, initialState);
53
const [startTransition, isPending] = useTransition({
47
- timeoutMs: 3000,
54
+ timeoutMs: 1500,
55
});
56
57
useEffect(() => {
@@ -56,12 +63,16 @@ function Router() {
63
startTransition(() => {
64
// TODO: Here, There, and Everywhere.
65
// TODO: Instant Transitions, somehow.
59
- // TODO: Buttons should update immediately.
66
dispatch({
61
- type: 'navigate',
67
+ type: 'completeNavigation',
68
+ RootBlock: loadApp(url),
69
url,
70
});
71
});
72
+ dispatch({
73
+ type: 'startNavigation',
74
+ url,
75
+ });
76
},
77
[startTransition]
78
);
@@ -76,10 +87,11 @@ function Router() {
87
88
const routeContext = useMemo(
89
() => ({
90
+ pendingUrl: state.pendingUrl,
91
url: state.url,
92
navigate,
93
}),
82
- [state.url, navigate]
94
+ [state.url, state.pendingUrl, navigate]
95
);
96
97
return (
fixtures/blocks/src/client/Link.js
new
+25
@@ -0,0 +1,25 @@
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
+export default function Link({to, children, ...rest}) {
12
+ const {navigate} = useRouter();
13
+ return (
14
+ <a
15
+ href={to}
16
+ onClick={e => {
17
+ e.preventDefault();
18
+ window.history.pushState(null, null, to);
19
+ navigate(to);
20
+ }}
21
+ {...rest}>
22
+ {children}
23
+ </a>
24
+ );
25
+}
fixtures/blocks/src/client/Shell.js
+10
-42
@@ -6,57 +6,25 @@
6
*/
7
8
import * as React from 'react';
9
-import {useRouter} from './RouterContext';
9
+import {TabBar, TabLink} from './TabNav';
10
11
-function TabBar({children}) {
12
- return (
13
- <div style={{border: '1px solid #aaa', padding: 20, width: 500}}>
14
- {children}
15
- </div>
16
- );
17
-}
11
+// TODO: Error Boundaries.
12
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
- }
13
+function MainTabNav() {
14
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>
15
+ <TabBar>
16
+ <TabLink to="/">Home</TabLink>
17
+ <TabLink to="/profile/3" partial={true}>
18
+ Profile
19
+ </TabLink>
20
+ </TabBar>
21
);
22
}
23
24
export default function Shell({children}) {
25
return (
26
<>
55
- <TabBar>
56
- <TabLink to="/">Home</TabLink>
57
- <TabLink to="/profile">Profile</TabLink>
58
- </TabBar>
59
- <br />
27
+ <MainTabNav />
28
{children}
29
</>
30
);
fixtures/blocks/src/client/TabNav.js
new
+50
@@ -0,0 +1,50 @@
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 Link from './Link';
10
+import {useRouter} from './RouterContext';
11
+
12
+export function TabBar({children}) {
13
+ return (
14
+ <div
15
+ style={{
16
+ border: '1px solid #aaa',
17
+ padding: 20,
18
+ marginBottom: 20,
19
+ width: 500,
20
+ }}>
21
+ {children}
22
+ </div>
23
+ );
24
+}
25
+
26
+export function TabLink({to, partial, children}) {
27
+ const {pendingUrl: activeUrl} = useRouter();
28
+ const active = partial ? activeUrl.startsWith(to) : activeUrl === to;
29
+ if (active) {
30
+ return (
31
+ <b
32
+ style={{
33
+ display: 'inline-block',
34
+ marginRight: 20,
35
+ }}>
36
+ {children}
37
+ </b>
38
+ );
39
+ }
40
+ return (
41
+ <Link
42
+ style={{
43
+ display: 'inline-block',
44
+ marginRight: 20,
45
+ }}
46
+ to={to}>
47
+ {children}
48
+ </Link>
49
+ );
50
+}
fixtures/blocks/src/server/App.block.js
+9
-9
@@ -16,15 +16,13 @@ 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');
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
}
@@ -33,6 +31,8 @@ function load(url) {
31
32
import Shell from '../client/Shell';
33
34
+// TODO: some notion of key.
35
+
36
function App(props, data) {
37
return (
38
<Shell>
fixtures/blocks/src/server/Comments.block.js
+9
-3
@@ -15,19 +15,25 @@ import {fetch} from 'react-data/fetch';
15
16
function load(postId) {
17
return {
18
- comments: fetch(`/comments?postId=${postId}`).json(),
18
+ comments: fetch(`/comments?postId=${postId}&_expand=user`).json(),
19
};
20
}
21
22
// Client
23
24
+import Link from '../client/Link';
25
+
26
function Comments(props, data) {
27
return (
28
<>
29
<h5>Comments</h5>
30
<ul>
29
- {data.comments.slice(0, 5).map(item => (
30
- <li key={item.id}>{item.body}</li>
31
+ {data.comments.slice(0, 5).map(comment => (
32
+ <li key={comment.id}>
33
+ {comment.body}
34
+ {' • '}
35
+ <Link to={`/profile/${comment.user.id}`}>{comment.user.name}</Link>
36
+ </li>
37
))}
38
</ul>
39
</>
fixtures/blocks/src/server/FeedPage.block.js
+1
-1
@@ -15,7 +15,7 @@ import {fetch} from 'react-data/fetch';
15
import PostList from './PostList';
16
17
function load(params) {
18
- const allPosts = fetch('/posts').json();
18
+ const allPosts = fetch('/posts?_expand=user').json();
19
return {
20
posts: <PostList posts={allPosts} />,
21
};
fixtures/blocks/src/server/Post.block.js
+9
-1
@@ -21,6 +21,8 @@ function load(postId) {
21
22
// Client
23
24
+import Link from '../client/Link';
25
+
26
function Post(props, data) {
27
return (
28
<div
@@ -31,7 +33,13 @@ function Post(props, data) {
33
padding: 20,
34
maxWidth: 500,
35
}}>
34
- <h4 style={{marginTop: 0}}>{props.post.title}</h4>
36
+ <h4 style={{marginTop: 0}}>
37
+ {props.post.title}
38
+ {' by '}
39
+ <Link to={`/profile/${props.post.user.id}`}>
40
+ {props.post.user.name}
41
+ </Link>
42
+ </h4>
43
<p>{props.post.body}</p>
44
<Suspense
45
fallback={<h5>Loading comments...</h5>}
fixtures/blocks/src/server/PostList.js
+1
-1
@@ -17,7 +17,7 @@ export default function PostList({posts}) {
17
return (
18
<SuspenseList revealOrder="forwards" tail="collapsed">
19
{posts.map(post => {
20
- preload(`/comments?postId=${post.id}`);
20
+ preload(`/comments?postId=${post.id}&_expand=user`);
21
const Post = loadPost(post.id);
22
return (
23
<Suspense key={post.id} fallback={<PostGlimmer />}>
fixtures/blocks/src/server/ProfileBio.block.js
new
+34
@@ -0,0 +1,34 @@
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
+
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) {
26
+ return (
27
+ <>
28
+ <h3>{data.user.name}'s Bio</h3>
29
+ <p>{data.bio.text}</p>
30
+ </>
31
+ );
32
+}
33
+
34
+export default block(ProfileBio, load);
fixtures/blocks/src/server/ProfilePage.block.js
+29
-5
@@ -12,23 +12,47 @@ import {block, Suspense} from 'react';
12
// Server
13
14
import {fetch} from 'react-data/fetch';
15
+import loadProfileBio from './ProfileBio.block';
16
import loadProfileTimeline from './ProfileTimeline.block';
17
17
-function load(userId) {
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 {
19
- user: fetch(`/users/${userId}`).json(),
20
- ProfileTimeline: loadProfileTimeline(userId),
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>
30
- <Suspense fallback={<h3>Loading Timeline...</h3>}>
31
- <data.ProfileTimeline />
53
+ <ProfileTabNav userId={data.user.id} />
54
+ <Suspense fallback={<h3>Loading...</h3>}>
55
+ <data.Tab />
56
</Suspense>
57
</>
58
);
fixtures/blocks/src/server/ProfileTimeline.block.js
+2
-7
@@ -15,7 +15,7 @@ import {fetch} from 'react-data/fetch';
15
import PostList from './PostList';
16
17
function load(userId) {
18
- const postsByUser = fetch(`/posts?userId=${userId}`).json();
18
+ const postsByUser = fetch(`/posts?userId=${userId}&_expand=user`).json();
19
return {
20
posts: <PostList posts={postsByUser} />,
21
};
@@ -24,12 +24,7 @@ function load(userId) {
24
// Client
25
26
function ProfileTimeline(props, data) {
27
- return (
28
- <>
29
- <h3>Timeline</h3>
30
- {data.posts}
31
- </>
32
- );
27
+ return <>{data.posts}</>;
28
}
29
30
export default block(ProfileTimeline, load);