diff --git a/fixtures/blocks/db.json b/fixtures/blocks/db.json index 8b2067d51a..1d7ec30679 100644 --- a/fixtures/blocks/db.json +++ b/fixtures/blocks/db.json @@ -23,37 +23,55 @@ { "id": 1, "body": "Hey there", - "postId": 1 + "postId": 1, + "userId": 1 }, { "id": 2, "body": "Welcome to the chat", - "postId": 1 + "postId": 1, + "userId": 2 }, { "id": 3, "body": "What editor/font are you using?", - "postId": 2 + "postId": 2, + "userId": 2 }, { "id": 4, "body": "It's always been hard", - "postId": 3 + "postId": 3, + "userId": 1 }, { "id": 5, "body": "It's still easy", - "postId": 3 + "postId": 3, + "userId": 2 } ], "users": [{ "id": 1, - "name": "Sebastian" + "name": "Sebastian", + "bioId": 10 }, { "id": 2, - "name": "Sophie" + "name": "Sophie", + "bioId": 20 }, { "id": 3, - "name": "Dan" + "name": "Dan", + "bioId": 30 + }], + "bios": [{ + "id": 10, + "text": "I like European movies" + }, { + "id": 20, + "text": "I like math puzzles" + }, { + "id": 30, + "text": "I like reading twitter" }] } diff --git a/fixtures/blocks/src/Router.js b/fixtures/blocks/src/Router.js index 6d3295004c..19fb76bd8e 100644 --- a/fixtures/blocks/src/Router.js +++ b/fixtures/blocks/src/Router.js @@ -24,17 +24,24 @@ const initialState = { // TODO: use this for invalidation. cache: createCache(), url: initialUrl, + pendingUrl: initialUrl, RootBlock: loadApp(initialUrl), }; function reducer(state, action) { switch (action.type) { - case 'navigate': + case 'startNavigation': + return { + ...state, + pendingUrl: action.url, + }; + case 'completeNavigation': // TODO: cancel previous fetch? return { - cache: state.cache, + ...state, url: action.url, - RootBlock: loadApp(action.url), + pendingUrl: action.url, + RootBlock: action.RootBlock, }; default: throw new Error(); @@ -44,7 +51,7 @@ function reducer(state, action) { function Router() { const [state, dispatch] = useReducer(reducer, initialState); const [startTransition, isPending] = useTransition({ - timeoutMs: 3000, + timeoutMs: 1500, }); useEffect(() => { @@ -56,12 +63,16 @@ function Router() { startTransition(() => { // TODO: Here, There, and Everywhere. // TODO: Instant Transitions, somehow. - // TODO: Buttons should update immediately. dispatch({ - type: 'navigate', + type: 'completeNavigation', + RootBlock: loadApp(url), url, }); }); + dispatch({ + type: 'startNavigation', + url, + }); }, [startTransition] ); @@ -76,10 +87,11 @@ function Router() { const routeContext = useMemo( () => ({ + pendingUrl: state.pendingUrl, url: state.url, navigate, }), - [state.url, navigate] + [state.url, state.pendingUrl, navigate] ); return ( diff --git a/fixtures/blocks/src/client/Link.js b/fixtures/blocks/src/client/Link.js new file mode 100644 index 0000000000..1f1f535da1 --- /dev/null +++ b/fixtures/blocks/src/client/Link.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react'; +import {useRouter} from './RouterContext'; + +export default function Link({to, children, ...rest}) { + const {navigate} = useRouter(); + return ( + { + e.preventDefault(); + window.history.pushState(null, null, to); + navigate(to); + }} + {...rest}> + {children} + + ); +} diff --git a/fixtures/blocks/src/client/Shell.js b/fixtures/blocks/src/client/Shell.js index e6e1a005f9..07b74ba88a 100644 --- a/fixtures/blocks/src/client/Shell.js +++ b/fixtures/blocks/src/client/Shell.js @@ -6,57 +6,25 @@ */ import * as React from 'react'; -import {useRouter} from './RouterContext'; +import {TabBar, TabLink} from './TabNav'; -function TabBar({children}) { - return ( -
- {children} -
- ); -} +// TODO: Error Boundaries. -function TabLink({to, children}) { - const {url: activeUrl, navigate} = useRouter(); - const active = activeUrl === to; - if (active) { - return ( - - {children} - - ); - } +function MainTabNav() { return ( - { - e.preventDefault(); - window.history.pushState(null, null, to); - navigate(to); - }}> - {children} - + + Home + + Profile + + ); } export default function Shell({children}) { return ( <> - - Home - Profile - -
+ {children} ); diff --git a/fixtures/blocks/src/client/TabNav.js b/fixtures/blocks/src/client/TabNav.js new file mode 100644 index 0000000000..142767263f --- /dev/null +++ b/fixtures/blocks/src/client/TabNav.js @@ -0,0 +1,50 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react'; +import Link from './Link'; +import {useRouter} from './RouterContext'; + +export function TabBar({children}) { + return ( +
+ {children} +
+ ); +} + +export function TabLink({to, partial, children}) { + const {pendingUrl: activeUrl} = useRouter(); + const active = partial ? activeUrl.startsWith(to) : activeUrl === to; + if (active) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); +} diff --git a/fixtures/blocks/src/server/App.block.js b/fixtures/blocks/src/server/App.block.js index 9347bbe0cf..a02cedd3ce 100644 --- a/fixtures/blocks/src/server/App.block.js +++ b/fixtures/blocks/src/server/App.block.js @@ -16,15 +16,13 @@ import loadProfilePage from './ProfilePage.block'; function load(url) { let Page; - switch (url) { - case '/': - Page = loadFeedPage(); - break; - case '/profile': - Page = loadProfilePage(3); - break; - default: - throw Error('Not found'); + const segments = url.split('/').filter(Boolean); + if (segments.length === 0) { + Page = loadFeedPage(); + } else if (segments[0] === 'profile') { + Page = loadProfilePage(Number(segments[1]), segments[2]); + } else { + throw Error('Not found'); } return {Page}; } @@ -33,6 +31,8 @@ function load(url) { import Shell from '../client/Shell'; +// TODO: some notion of key. + function App(props, data) { return ( diff --git a/fixtures/blocks/src/server/Comments.block.js b/fixtures/blocks/src/server/Comments.block.js index 3e021519fd..fdea53adee 100644 --- a/fixtures/blocks/src/server/Comments.block.js +++ b/fixtures/blocks/src/server/Comments.block.js @@ -15,19 +15,25 @@ import {fetch} from 'react-data/fetch'; function load(postId) { return { - comments: fetch(`/comments?postId=${postId}`).json(), + comments: fetch(`/comments?postId=${postId}&_expand=user`).json(), }; } // Client +import Link from '../client/Link'; + function Comments(props, data) { return ( <>
Comments
diff --git a/fixtures/blocks/src/server/FeedPage.block.js b/fixtures/blocks/src/server/FeedPage.block.js index 56ebcaffe2..9fdbb414fe 100644 --- a/fixtures/blocks/src/server/FeedPage.block.js +++ b/fixtures/blocks/src/server/FeedPage.block.js @@ -15,7 +15,7 @@ import {fetch} from 'react-data/fetch'; import PostList from './PostList'; function load(params) { - const allPosts = fetch('/posts').json(); + const allPosts = fetch('/posts?_expand=user').json(); return { posts: , }; diff --git a/fixtures/blocks/src/server/Post.block.js b/fixtures/blocks/src/server/Post.block.js index 23ce58cbd3..ffc3e63b38 100644 --- a/fixtures/blocks/src/server/Post.block.js +++ b/fixtures/blocks/src/server/Post.block.js @@ -21,6 +21,8 @@ function load(postId) { // Client +import Link from '../client/Link'; + function Post(props, data) { return (
-

{props.post.title}

+

+ {props.post.title} + {' by '} + + {props.post.user.name} + +

{props.post.body}

Loading comments...} diff --git a/fixtures/blocks/src/server/PostList.js b/fixtures/blocks/src/server/PostList.js index 2baf1da4fc..f479da5eb2 100644 --- a/fixtures/blocks/src/server/PostList.js +++ b/fixtures/blocks/src/server/PostList.js @@ -17,7 +17,7 @@ export default function PostList({posts}) { return ( {posts.map(post => { - preload(`/comments?postId=${post.id}`); + preload(`/comments?postId=${post.id}&_expand=user`); const Post = loadPost(post.id); return ( }> diff --git a/fixtures/blocks/src/server/ProfileBio.block.js b/fixtures/blocks/src/server/ProfileBio.block.js new file mode 100644 index 0000000000..1142524de2 --- /dev/null +++ b/fixtures/blocks/src/server/ProfileBio.block.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* eslint-disable import/first */ + +import * as React from 'react'; +import {block} from 'react'; + +// Server + +import {fetch} from 'react-data/fetch'; + +function load(user) { + return { + user, + bio: fetch(`/bios/${user.bioId}`).json(), + }; +} + +// Client + +function ProfileBio(props, data) { + return ( + <> +

{data.user.name}'s Bio

+

{data.bio.text}

+ + ); +} + +export default block(ProfileBio, load); diff --git a/fixtures/blocks/src/server/ProfilePage.block.js b/fixtures/blocks/src/server/ProfilePage.block.js index 6ee61e56b4..668c44b3d7 100644 --- a/fixtures/blocks/src/server/ProfilePage.block.js +++ b/fixtures/blocks/src/server/ProfilePage.block.js @@ -12,23 +12,47 @@ import {block, Suspense} from 'react'; // Server import {fetch} from 'react-data/fetch'; +import loadProfileBio from './ProfileBio.block'; import loadProfileTimeline from './ProfileTimeline.block'; -function load(userId) { +function load(userId, tab) { + const user = fetch(`/users/${userId}`).json(); + let Tab; + switch (tab) { + case 'bio': + Tab = loadProfileBio(user); + break; + default: + Tab = loadProfileTimeline(userId); + break; + } return { - user: fetch(`/users/${userId}`).json(), - ProfileTimeline: loadProfileTimeline(userId), + Tab, + user, }; } // Client +import {TabBar, TabLink} from '../client/TabNav'; + +function ProfileTabNav({userId}) { + // TODO: Don't hardcode ID. + return ( + + Timeline + Bio + + ); +} + function ProfilePage(props, data) { return ( <>

{data.user.name}

- Loading Timeline...}> - + + Loading...}> + ); diff --git a/fixtures/blocks/src/server/ProfileTimeline.block.js b/fixtures/blocks/src/server/ProfileTimeline.block.js index c1c0978a16..8d42eda7a5 100644 --- a/fixtures/blocks/src/server/ProfileTimeline.block.js +++ b/fixtures/blocks/src/server/ProfileTimeline.block.js @@ -15,7 +15,7 @@ import {fetch} from 'react-data/fetch'; import PostList from './PostList'; function load(userId) { - const postsByUser = fetch(`/posts?userId=${userId}`).json(); + const postsByUser = fetch(`/posts?userId=${userId}&_expand=user`).json(); return { posts: , }; @@ -24,12 +24,7 @@ function load(userId) { // Client function ProfileTimeline(props, data) { - return ( - <> -

Timeline

- {data.posts} - - ); + return <>{data.posts}; } export default block(ProfileTimeline, load);