[Blocks Fixture] Misc updates (#18811)

* [Blocks Fixture] Update navigation buttons immediately

* Add more profile links

* Minor refactor

* Add subroutes to Profile
This commit is contained in:
Dan Abramov 2020-05-05 11:53:02 +01:00 committed by GitHub
parent fe7163e73d
commit 4d124a4f67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 224 additions and 84 deletions

View File

@ -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"
}]
}

View File

@ -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 (

View File

@ -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 (
<a
href={to}
onClick={e => {
e.preventDefault();
window.history.pushState(null, null, to);
navigate(to);
}}
{...rest}>
{children}
</a>
);
}

View File

@ -6,57 +6,25 @@
*/
import * as React from 'react';
import {useRouter} from './RouterContext';
import {TabBar, TabLink} from './TabNav';
function TabBar({children}) {
return (
<div style={{border: '1px solid #aaa', padding: 20, width: 500}}>
{children}
</div>
);
}
// TODO: Error Boundaries.
function TabLink({to, children}) {
const {url: activeUrl, navigate} = useRouter();
const active = activeUrl === to;
if (active) {
function MainTabNav() {
return (
<b
style={{
display: 'inline-block',
width: 50,
marginRight: 20,
}}>
{children}
</b>
);
}
return (
<a
style={{
display: 'inline-block',
width: 50,
marginRight: 20,
}}
href={to}
onClick={e => {
e.preventDefault();
window.history.pushState(null, null, to);
navigate(to);
}}>
{children}
</a>
<TabBar>
<TabLink to="/">Home</TabLink>
<TabLink to="/profile/3" partial={true}>
Profile
</TabLink>
</TabBar>
);
}
export default function Shell({children}) {
return (
<>
<TabBar>
<TabLink to="/">Home</TabLink>
<TabLink to="/profile">Profile</TabLink>
</TabBar>
<br />
<MainTabNav />
{children}
</>
);

View File

@ -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 (
<div
style={{
border: '1px solid #aaa',
padding: 20,
marginBottom: 20,
width: 500,
}}>
{children}
</div>
);
}
export function TabLink({to, partial, children}) {
const {pendingUrl: activeUrl} = useRouter();
const active = partial ? activeUrl.startsWith(to) : activeUrl === to;
if (active) {
return (
<b
style={{
display: 'inline-block',
marginRight: 20,
}}>
{children}
</b>
);
}
return (
<Link
style={{
display: 'inline-block',
marginRight: 20,
}}
to={to}>
{children}
</Link>
);
}

View File

@ -16,14 +16,12 @@ import loadProfilePage from './ProfilePage.block';
function load(url) {
let Page;
switch (url) {
case '/':
const segments = url.split('/').filter(Boolean);
if (segments.length === 0) {
Page = loadFeedPage();
break;
case '/profile':
Page = loadProfilePage(3);
break;
default:
} 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 (
<Shell>

View File

@ -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 (
<>
<h5>Comments</h5>
<ul>
{data.comments.slice(0, 5).map(item => (
<li key={item.id}>{item.body}</li>
{data.comments.slice(0, 5).map(comment => (
<li key={comment.id}>
{comment.body}
{' • '}
<Link to={`/profile/${comment.user.id}`}>{comment.user.name}</Link>
</li>
))}
</ul>
</>

View File

@ -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: <PostList posts={allPosts} />,
};

View File

@ -21,6 +21,8 @@ function load(postId) {
// Client
import Link from '../client/Link';
function Post(props, data) {
return (
<div
@ -31,7 +33,13 @@ function Post(props, data) {
padding: 20,
maxWidth: 500,
}}>
<h4 style={{marginTop: 0}}>{props.post.title}</h4>
<h4 style={{marginTop: 0}}>
{props.post.title}
{' by '}
<Link to={`/profile/${props.post.user.id}`}>
{props.post.user.name}
</Link>
</h4>
<p>{props.post.body}</p>
<Suspense
fallback={<h5>Loading comments...</h5>}

View File

@ -17,7 +17,7 @@ export default function PostList({posts}) {
return (
<SuspenseList revealOrder="forwards" tail="collapsed">
{posts.map(post => {
preload(`/comments?postId=${post.id}`);
preload(`/comments?postId=${post.id}&_expand=user`);
const Post = loadPost(post.id);
return (
<Suspense key={post.id} fallback={<PostGlimmer />}>

View File

@ -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 (
<>
<h3>{data.user.name}'s Bio</h3>
<p>{data.bio.text}</p>
</>
);
}
export default block(ProfileBio, load);

View File

@ -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 (
<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>
<Suspense fallback={<h3>Loading Timeline...</h3>}>
<data.ProfileTimeline />
<ProfileTabNav userId={data.user.id} />
<Suspense fallback={<h3>Loading...</h3>}>
<data.Tab />
</Suspense>
</>
);

View File

@ -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: <PostList posts={postsByUser} />,
};
@ -24,12 +24,7 @@ function load(userId) {
// Client
function ProfileTimeline(props, data) {
return (
<>
<h3>Timeline</h3>
{data.posts}
</>
);
return <>{data.posts}</>;
}
export default block(ProfileTimeline, load);