[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
This commit is contained in:
Dan Abramov
2020-05-06 05:17:44 +01:00
committed by GitHub
parent 823dc581fe
commit 595d27bd73
18 changed files with 272 additions and 222 deletions
+6 -9
View File
@@ -15,17 +15,16 @@ import React, {
} from 'react';
import {createCache, CacheProvider} from 'react/unstable-cache';
import {RouterProvider} from './client/RouterContext';
// TODO: entry point. This can't really done in the client code.
import loadApp from './server/App.block';
// TODO: can't really import a server component on the client.
import App from './server/App';
const initialUrl = window.location.pathname;
const initialState = {
// TODO: use this for invalidation.
cache: createCache(),
url: initialUrl,
pendingUrl: initialUrl,
RootBlock: loadApp(initialUrl),
root: <App route={initialUrl} />,
};
function reducer(state, action) {
@@ -41,7 +40,7 @@ function reducer(state, action) {
...state,
url: action.url,
pendingUrl: action.url,
RootBlock: action.RootBlock,
root: action.root,
};
default:
throw new Error();
@@ -65,7 +64,7 @@ function Router() {
// TODO: Instant Transitions, somehow.
dispatch({
type: 'completeNavigation',
RootBlock: loadApp(url),
root: <App route={url} />,
url,
});
});
@@ -97,9 +96,7 @@ function Router() {
return (
<Suspense fallback={<h2>Loading...</h2>}>
<CacheProvider value={state.cache}>
<RouterProvider value={routeContext}>
<state.RootBlock />
</RouterProvider>
<RouterProvider value={routeContext}>{state.root}</RouterProvider>
</CacheProvider>
</Suspense>
);
+21
View File
@@ -0,0 +1,21 @@
/**
* 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 {TabBar, TabLink} from '../client/TabNav';
export default function ProfileNav({userId}) {
// TODO: Don't hardcode ID.
return (
<TabBar>
<TabLink to={`/profile/${userId}`}>Timeline</TabLink>
<TabLink to={`/profile/${userId}/bio`}>Bio</TabLink>
</TabBar>
);
}
-44
View File
@@ -1,44 +0,0 @@
/**
* 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 {unstable_block as block} from 'react';
// Server
import loadFeedPage from './FeedPage.block';
import loadProfilePage from './ProfilePage.block';
function load(url) {
let Page;
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};
}
// Client
import Shell from '../client/Shell';
// TODO: some notion of key.
function App(props, data) {
return (
<Shell>
<data.Page />
</Shell>
);
}
export default block(App, load);
+29
View File
@@ -0,0 +1,29 @@
/**
* 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 {matchRoute} from './ServerRouter';
import FeedPage from './FeedPage';
import ProfilePage from './ProfilePage';
// TODO: Replace with asset reference.
import loadShell from './Shell.block';
// TODO: Router component?
const AppRoutes = {
'/': props => <FeedPage {...props} key="home" />,
'/profile/:userId/*': props => (
<ProfilePage {...props} key={`profile-${props.userId}`} />
),
};
export default function App(props) {
const Shell = loadShell();
const match = matchRoute(props, AppRoutes);
return <Shell>{match}</Shell>;
}
@@ -7,28 +7,19 @@
/* eslint-disable import/first */
import * as React from 'react';
import {unstable_block as block} from 'react';
// Server
import {fetch} from 'react-data/fetch';
function load(postId) {
return {
comments: fetch(`/comments?postId=${postId}&_expand=user`).json(),
};
}
// TODO: Replace with asset reference.
import loadLink from './Link.block';
// Client
import Link from '../client/Link';
function Comments(props, data) {
export default function Comments({postId}) {
const Link = loadLink();
const comments = fetch(`/comments?postId=${postId}&_expand=user`).json();
return (
<>
<h5>Comments</h5>
<ul>
{data.comments.slice(0, 5).map(comment => (
{comments.slice(0, 5).map(comment => (
<li key={comment.id}>
{comment.body}
{' • '}
@@ -39,5 +30,3 @@ function Comments(props, data) {
</>
);
}
export default block(Comments, load);
@@ -7,29 +7,15 @@
/* eslint-disable import/first */
import * as React from 'react';
import {unstable_block as block} from 'react';
import {fetch} from 'react-data/fetch';
// Server
import PostList from './PostList';
function load(params) {
const allPosts = fetch('/posts?_expand=user').json();
return {
posts: <PostList posts={allPosts} />,
};
}
// Client
function FeedPage(props, data) {
export default function Feed() {
const posts = fetch('/posts?_expand=user').json();
return (
<>
<h2>Feed</h2>
{data.posts}
<PostList posts={posts} />
</>
);
}
export default block(FeedPage, load);
+20
View File
@@ -0,0 +1,20 @@
/**
* 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 {Suspense} from 'react';
import PostGlimmer from './PostGlimmer';
import Feed from './Feed';
export default function FeedPage() {
return (
<Suspense fallback={<PostGlimmer />}>
<Feed />
</Suspense>
);
}
+22
View File
@@ -0,0 +1,22 @@
/**
* 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 {unstable_block as block} from 'react';
// Client
// TODO: delete this wrapper.
import ClientLink from '../client/Link';
function Link(props, _) {
return <ClientLink {...props} />;
}
export default block(Link);
@@ -7,23 +7,14 @@
/* eslint-disable import/first */
import * as React from 'react';
import {unstable_block as block, Suspense} from 'react';
import {Suspense} from 'react';
import Comments from './Comments';
// Server
// TODO: Replace with asset reference.
import loadLink from './Link.block';
import loadComments from './Comments.block';
function load(postId) {
return {
Comments: loadComments(postId),
};
}
// Client
import Link from '../client/Link';
function Post(props, data) {
export default function Post({post}) {
const Link = loadLink();
return (
<div
style={{
@@ -34,20 +25,16 @@ function Post(props, data) {
maxWidth: 500,
}}>
<h4 style={{marginTop: 0}}>
{props.post.title}
{post.title}
{' by '}
<Link to={`/profile/${props.post.user.id}`}>
{props.post.user.name}
</Link>
<Link to={`/profile/${post.user.id}`}>{post.user.name}</Link>
</h4>
<p>{props.post.body}</p>
<p>{post.body}</p>
<Suspense
fallback={<h5>Loading comments...</h5>}
unstable_avoidThisFallback={true}>
<data.Comments />
<Comments postId={post.id} />
</Suspense>
</div>
);
}
export default block(Post, load);
+1 -3
View File
@@ -9,16 +9,14 @@
import * as React from 'react';
import {Suspense, unstable_SuspenseList as SuspenseList} from 'react';
import {preload} from 'react-data/fetch';
import loadPost from './Post.block';
import PostGlimmer from './PostGlimmer';
import Post from './Post';
export default function PostList({posts}) {
return (
<SuspenseList revealOrder="forwards" tail="collapsed">
{posts.map(post => {
preload(`/comments?postId=${post.id}&_expand=user`);
const Post = loadPost(post.id);
return (
<Suspense key={post.id} fallback={<PostGlimmer />}>
<Post post={post} />
@@ -7,28 +7,15 @@
/* eslint-disable import/first */
import * as React from 'react';
import {unstable_block as 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) {
export default function ProfileBio({userId}) {
const user = fetch(`/users/${userId}`).json();
const bio = fetch(`/bios/${user.bioId}`).json().text;
return (
<>
<h3>{data.user.name}'s Bio</h3>
<p>{data.bio.text}</p>
<h3>{user.name}'s Bio</h3>
<p>{bio}</p>
</>
);
}
export default block(ProfileBio, load);
@@ -0,0 +1,22 @@
/**
* 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 {unstable_block as block} from 'react';
// Client
// TODO: delete this wrapper.
import ClientProfileNav from '../client/ProfileNav';
function ProfileNav(props, _) {
return <ClientProfileNav {...props} />;
}
export default block(ProfileNav);
@@ -1,61 +0,0 @@
/**
* 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 {unstable_block as block, Suspense} from 'react';
// Server
import {fetch} from 'react-data/fetch';
import loadProfileBio from './ProfileBio.block';
import loadProfileTimeline from './ProfileTimeline.block';
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 {
Tab,
user,
};
}
// Client
import {TabBar, TabLink} from '../client/TabNav';
function ProfileTabNav({userId}) {
// TODO: Don't hardcode ID.
return (
<TabBar>
<TabLink to={`/profile/${userId}`}>Timeline</TabLink>
<TabLink to={`/profile/${userId}/bio`}>Bio</TabLink>
</TabBar>
);
}
function ProfilePage(props, data) {
return (
<>
<h2>{data.user.name}</h2>
<ProfileTabNav userId={data.user.id} />
<Suspense fallback={<h3>Loading...</h3>}>
<data.Tab />
</Suspense>
</>
);
}
export default block(ProfilePage, load);
+35
View File
@@ -0,0 +1,35 @@
/**
* 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 {Suspense} from 'react';
import {fetch} from 'react-data/fetch';
import {matchRoute} from './ServerRouter';
import ProfileTimeline from './ProfileTimeline';
import ProfileBio from './ProfileBio';
// TODO: Replace with asset reference.
import loadProfileNav from './ProfileNav.block';
// TODO: Router component?
const ProfileRoutes = {
'/': props => <ProfileTimeline {...props} key="timeline" />,
'/bio': props => <ProfileBio {...props} key="bio" />,
};
export default function ProfilePage(props) {
const user = fetch(`/users/${props.userId}`).json();
const ProfileNav = loadProfileNav();
const match = matchRoute(props, ProfileRoutes);
return (
<>
<h2>{user.name}</h2>
<ProfileNav userId={user.id} />
<Suspense fallback={<h3>Loading...</h3>}>{match}</Suspense>
</>
);
}
@@ -1,30 +0,0 @@
/**
* 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 {unstable_block as block} from 'react';
// Server
import {fetch} from 'react-data/fetch';
import PostList from './PostList';
function load(userId) {
const postsByUser = fetch(`/posts?userId=${userId}&_expand=user`).json();
return {
posts: <PostList posts={postsByUser} />,
};
}
// Client
function ProfileTimeline(props, data) {
return <>{data.posts}</>;
}
export default block(ProfileTimeline, load);
@@ -0,0 +1,16 @@
/**
* 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 {fetch} from 'react-data/fetch';
import PostList from './PostList';
export default function ProfileTimeline({userId}) {
const posts = fetch(`/posts?userId=${userId}&_expand=user`).json();
return <PostList posts={posts} />;
}
@@ -0,0 +1,54 @@
/**
* 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 */
function tryMatch(props, def) {
const defSegments = def.split('/').filter(Boolean);
const routeSegments = props.route.split('/').filter(Boolean);
let innerProps = {...props};
while (routeSegments.length > 0) {
if (defSegments.length === 0) {
return null;
}
const urlSegment = routeSegments.shift();
const defSegment = defSegments.shift();
if (urlSegment === defSegment) {
continue;
}
if (defSegment[0] === ':') {
innerProps[defSegment.slice(1)] = urlSegment;
continue;
}
if (defSegment === '*') {
innerProps.route = '/' + urlSegment + routeSegments.join('/');
return innerProps;
}
return null;
}
if (defSegments.length === 0) {
return innerProps;
}
if (defSegments.length === 1 && defSegments[0] === '*') {
innerProps.route = '/';
return innerProps;
}
return null;
}
export function matchRoute(props, defs) {
for (let def in defs) {
if (!defs.hasOwnProperty(def)) {
continue;
}
const innerProps = tryMatch(props, def);
if (innerProps) {
const match = defs[def](innerProps);
return match;
}
}
throw Error('Not found.');
}
+22
View File
@@ -0,0 +1,22 @@
/**
* 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 {unstable_block as block} from 'react';
// Client
// TODO: delete this wrapper.
import ClientShell from '../client/Shell';
function Shell(props, _) {
return <ClientShell {...props} />;
}
export default block(Shell);