Skip to content

Instantly share code, notes, and snippets.

@jamesknelson
Last active March 19, 2019 11:22
Show Gist options
  • Save jamesknelson/0a6ddb2807b11051859972b2f48668b9 to your computer and use it in GitHub Desktop.
Save jamesknelson/0a6ddb2807b11051859972b2f48668b9 to your computer and use it in GitHub Desktop.
Two APIs for routing with React that support POST methods and SSR.
/**
UPDATE:
This component and hook based routing/fetching API won't work, as `useAsync()`
is an impossible component.
In order to use async functions to respond to route changes, the functions will
need to be registered with a parent cache/provider with a unique key. As such, a
more natural component-based architecture would involve a `<Route path>` component
that specifies any dependencies.
For details on why, see: https://twitter.com/james_k_nelson/status/1107932897900552194
**/
/*
* Perform routing with methods on both the client and server, using a
* hypothetical component/hook based API inspired by @reach/router.
*
* Pros:
* - It's "just React"
* - Can use React context
*
* Cons:
* - Can't build a list of URLs or find their route details at runtime
* - Hook-based routing logic is (in my opinion) harder to follow
* - Won't work with suspense until streaming SSR (late this year or next year)
*/
// App.js
const Login = React.lazy(() => import('./Login'))
const App = () =>
<Router>
<Login path='/login' />
</Router>
// Login.js
const LoginRoute = ({ request }) => {
let auth = useAuth()
// Store any error on window.history.state, so that a request whose email/
// password is incorrect won't be re-run on forward/back.
let [error, setError] = useStateSerializedToHistory('loginerror')
// Wait for the argument function to resolve, re-running it each
// time `request` or `error` changes.
let loginSucceeded = useAsync(async () => {
if (request.method === 'post' && !error) {
try {
let { email, password } = request.body
await auth.signInWithEmailAndPassword(email, password)
}
catch (error) {
setError(error)
}
}
}, [request, error])
if (loginSucceeded) {
return (
<Redirect to='/dashboard' />
)
}
return <>
<Status code={error ? 400 : 200} error={error} />
<Head>
<title>Login</title>
</Head>
<Login />
</>
}
function Login() {
// Bind the form's state to window.history.state, so that accidentally
// going back/forward will not result in the state being lost
let [model, setModel] = useStateSerializedToHistory('loginform')
return (
<Form method='post' initialValue={model} onChange={setModel}>
<Form.Errors />
<Form.Field name='email' type='email' />
<Form.Field name='password' type='password' />
<Form.SubmitButton>
Login
</Form.SubmitButton>
</Form>
)
}
export default LoginRoute
/*
* Perform routing on the client and server, using Navi's routing
* existing API.
*
* Pros:
* - Works server-side right now, doesn't need streaming SSR
* - Can build a list of the site's URLs and routing data at runtime
* - Unlike hooks, you can use if/else/async/await/try/catch without restriction
*
* Cons:
* - Navi's routing functions aren't "just React"
* - Unable to access React context or props within routing code
*/
// App.js
const routes = mount({
'/login': lazy(() => import('./Login'))
})
const App = () =>
<Router routes={routes}>
<View />
</Router>
// Login.js
export default map(async request => {
let auth = request.context.auth
let state = { ...request.state }
// Store any error on window.history.state, so that a request whose email/
// password is incorrect won't be re-run on forward/back.
if (request.method === 'post' && !state.error) {
try {
let { email, password } = request.body
await auth.signInWithEmailAndPassword(email, password)
return redirect('/dashboard')
}
catch (error) {
state.error = e
}
}
return route({
error: state.error,
status: state.error ? 400 : 200,
title: 'Login',
view: <Login />,
// Set's the value of history.state if this request is re-rendered due
// to forward/back buttons, or after pre-rendering on the server.
state,
})
})
const Login = () => {
// Bind the form's state to window.history.state, so that accidentally
// going back/forward will not result in the state being lost
let [model, setModel] = useStateSerializedToHistory('loginform')
return (
<Form method='post' initialValue={model} onChange={setModel}>
<Form.Errors />
<Form.Field name='email' type='email' />
<Form.Field name='password' type='password' />
<Form.SubmitButton>
Login
</Form.SubmitButton>
</Form>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment