

A React with Hooks and Suspense
source link: https://www.tuicool.com/articles/hit/FFJF32m
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

So the React team just released a new API called Hooks . It’s amazing. It lets you declaratively model state and side effects. You’ve probably read about it elsewhere on the internet, so I’m not going to tell you about hooks themselves, but…
With a new API comes new possibilities. And to cut to the chase, Navi’s new <Router>
component uses Hooks and Suspense to make routing simpler than ever before. It makes all sorts of things possible — you can even add animated loading transitions in just 3 lines of code
.
So how do you use these new superpowered hooks? We’ll get to that in a moment. But before we do,
what the hell is a <Router>
?
How many routes could a
<Router routes />
route…
It can route as many as you’d like, because Navi lets you dynamically import()
entire routing trees on demand. But how
?
The trick is in Navi’s method for declaring routes. For simple routes, you can just use Navi’s mount()
and route()
functions. But for heavier content, you can declare dependencies on asynchronous data and views using async
/ await
— or you can even split out entire routing trees using lazy()
.
<Router routes={ mount({ '/': route({ title: 'My Shop', getData: () => api.fetchProducts(), view: <Landing />, }), '/products': lazy(() => import('./productsRoutes')), }) } />
If you take a look at this example, you’ll see that you’ve got yourself a <Router>
with a couple routes, including a shop’s landing page and a lazily loadable /products
URL.
Let’s build the rest of the shop.
For your next step, you’ll need to decide where
to render the current route’s view
element. And to do that, you just plonk down a <View />
element somewhere inside your <Router>
.
ReactDOM.render( <Router routes={routes}> <Layout> <View /> </Layout> </Router>, document.getElementById('root') )
Simple, huh? But waaait a minute… what if you view the lazily loadable /products
URL? Then the route will be loaded via an import()
, which returns a Promise, and so at first there’ll be nothing to render. Luckily, React’s new
<Suspense>
feature lets you declaratively wait for promises to resolve. So just wrap your <View>
in a <Suspense>
tag, and you’re off and racing!
index.js
product.js
Landing.js
Layout.js
api.js
styles.css
import { mount, route, lazy } from 'navi' import React, { Suspense } from 'react' import ReactDOM from 'react-dom' import { Router, View } from 'react-navi' import api from './api' import Landing from './Landing' import Layout from './Layout' const routes = mount({ '/': route({ title: "Hats 'n' Flamethrowers 'r' Us", getData: () => api.fetchProducts(), view: <Landing />, }), '/product': lazy(() => import('./product')), }) ReactDOM.render( <Router routes={routes}> <Layout> <Suspense fallback={null}> <View /> </Suspense> </Layout> </Router>, document.getElementById('root') )
Bro, just give me the hooks?
Ok, so you’ve seen how to render a route’s view. But did you notice that your route also defines a getData()
function?
route({ title: 'My Shop', getData: () => api.fetch('/products'), view: <Landing />, })
How do you access the data? With React hooks!
Navi’s useCurrentRoute()
hook can be called from any function component that is rendered within the <Router>
tag. It returns a
Route
object that contains everything that Navi knows about the current URL.
index.js
product.js
Landing.js
Layout.js
api.js
styles.css
import React from 'react' import { Link, useCurrentRoute } from 'react-navi' export default function Landing() { // useCurrentRoute returns the lastest loaded Route object let route = useCurrentRoute() let data = route.data let productIds = Object.keys(data) console.log('views', route.views) console.log('url', route.url) console.log('data', route.data) console.log('status', route.status) return ( <ul> {productIds.map(id => <li key={id}> <Link href={`/product/${id}`}>{data[id].title}</Link> </li> )} </ul> ) }
Ok. So far, so good. But imagine that you’ve just clicked a link to /products
— which is dynamically imported. It’s going to take some time to fetch the route, so what are you going to display in the meantime?
Visualizing loading routes
When routes take a long time to load, you’ll want to display some sort of loading indicator to the user — and there a number of approaches that you could
take. One option would be to show a fallback with <Suspense>
, just as with the initial load. But this looks a bit shit.

What you’d really like to do is to display a loading bar over the current page while the next route loads… well, unless the transition only takes 100ms. Then you probably just want to keep displaying the current page until the next one is ready, because showing a loading bar for only 100ms also looks a bit shit.

There’s just one problem. Doing this with currently available tools is ridiculously hard, right? Well actually… You can add it to the above demo in just 3 lines of code, using the useLoadingRoute()
hook and the react-busy-indicator
package.
index.js
product.js
Landing.js
Layout.js
api.js
styles.css
import BusyIndicator from '[email protected]' import React from 'react' import { Link, useLoadingRoute } from 'react-navi' export default function Layout({ children }) { // If there is a route that hasn't finished loading, it can be // retrieved with `useLoadingRoute()`. let loadingRoute = useLoadingRoute() return ( <div className="Layout"> {/* This component shows a loading indicator after a delay */} <BusyIndicator isBusy={!!loadingRoute} delayMs={200} /> <header className="Layout-header"> <h1 className="Layout-title"> <Link href='/' prefetch={null}> Hats 'n' Flamethrowers 'r' Us </Link> </h1> </header> <main> {children} </main> </div> ) }
Go ahead and try clicking between these pages a few times. Did you notice how smooth the transition back to the index page is? No? It was so smooth that you didn’t notice that there’s actually a 100ms delay? Great! That’s exactly the experience that your users want.
Here’s how it works: useCurrentRoute()
returns the most recent completely loaded
route. And useLoadingRoute()
returns any requested-but-not-yet-completely-loaded route. Or if the user hasn’t
just clicked a link, it returns undefined
.
Want to display a loading bar while pages load? Then just call useLoadingRoute()
, check if there’s a value, and render a loading bar if there is! CSS transitions let you do the rest.
I’m not going to drop the entire set ofguides,API reference, and docs on integrating with other tools on you right now. You’re reading a blog post, so you might not have time for all that juicy information. But let me ask you a question:
What happens if the route doesn’t load?
One of the things about asynchronous data and views is that sometimes they don’t bloody work . Luckily, React has a great tool for dealing with things that don’t bloody work: Error Boundaries.
Let’s rewind for a moment to the <Suspense>
tag that wraps your <View />
. When <View />
encounters a not-yet-loaded route, it throws a promise
, which effectively asks React to please show the fallback for a moment
. You can imagine that <Suspense>
catches that promise, and then re-renders its children once the promise resolves.
Similarly, if <View />
finds that getView()
or getData()
have thrown an error, then it re-throws that error. In fact, if the router encounters a 404-page-gone-for-a-long-stroll error, then <View />
will throw that, too. These errors can be caught by Error Boundary
components. For the most part, you’ll need to make your own error boundaries, but Navi includes a
<NotFoundBoundary>
to show you how its done:
index.js
product.js
Landing.js
Layout.js
api.js
styles.css
import BusyIndicator from '[email protected]' import React from 'react' import { Link, NotFoundBoundary, useLoadingRoute } from 'react-navi' export default function Layout({ children }) { // If there is a route that hasn't finished loading, it can be // retrieved with `useLoadingRoute()`. let loadingRoute = useLoadingRoute() return ( <div className="Layout"> {/* This component shows a loading indicator after a delay */} <BusyIndicator isBusy={!!loadingRoute} delayMs={200} /> <header className="Layout-header"> <h1 className="Layout-title"> <Link href='/'> Hats 'n' Flamethrowers 'r' Us </Link> </h1> </header> <main> <NotFoundBoundary render={renderNotFound}> {children} </NotFoundBoundary> </main> </div> ) } function renderNotFound() { return ( <div className='Layout-error'> <h1>404 - Not Found</h1> </div> ) }
But that’s not all!
Ok, so I think we’re about out of time for this blog post. But there’s a bunch more details in the docs:
- Using URL Parameters
- Requests, Routes and Matchers
- Nested Routes and Views
- Programmatic navigation
- Guarding routes with authentication
- Improving SEO with static rendering
- Using Navi with react-router
- Using Navi with react-helmet
- Get a head start with a create-react-app based starter
You can also check out the examples directory of the Navi repository for full code examples, including:
- A basic site with create-react-app
- A simple site with static rendering
- A blog with tags, pagination and RSS
- That same blog but written in TypeScript
Oh, and did I mention that Navi is build with TypeScript, so the typings are first-class?
Help me, Obi-Wan Kenobi. You’re my only hope.
Ok, so even if you’re not Obi-Wan Kenobi, I’d really appreciate your help.
Actually, a lot of little things are way more helpful than they might seem. Can you try Navi out and file an issue for any missing features? Awesome. Can you make tiny improvements to the docs as you learn? Radical. Can you put something small online and add it to the README 's list of sites using Navi? Phenomenal.
With that said, there are a few big ticket items that I’d love some help with:
-
Navi needs some dev tools. Internally, all data is represented as simple objects calledChunks — which are then reduced into
Route
objects with a Redux-like reducer. As a result of this design, it should be possible for dev tools to provide a useful window into what’s going on, along with time-travel capability — but I want to leave this task for the community. So if you want to be the person who made Navi’s dev tools, let’s discuss the details -
Matcher functions like
mount()
,lazy()
androute()
are just Generator Functions. In fact, it’s entirely possible to create custom matchers . For instance, you could create awithTimeout()
matcher that switches routes based on how long they take to load. And if you do create useful custom matchers, send a tweet or DM to @james_k_nelson so I can spread the word -
If you can provide a translation for even a single page from the docs, I’ll be forever grateful :heart:
-
If you’d like to see this project grow, please give it a :star2:Star on Github !
Navi now has experimental server rendering support. Matchers like route()
and mount()
have access to an entire Request
object, including method
, headers
, and body
. Your routes aren’t limited to matching just a view
and data
— they can also match a HTTP status
and headers
.
You can now handle routing on the client, the server and in serverless functions with exactly the same code.
Of course, you could already do some of this with Next.js — but if all you need is a router, then Navi is a lot smaller (and more flexible). If you’d like to know more about the difference, take a read throughNavi vs. Next.js. Or if you just want the server rendering demo:
index.js
product.js
Landing.js
Layout.js
api.js
styles.css
import { createMemoryNavigation, mount, route, lazy } from 'navi' import React, { Suspense } from 'react' import ReactDOMServer from 'react-dom/server' import { NaviProvider, View } from 'react-navi' import api from './api' import Landing from './Landing' import Layout from './Layout' async function main() { const routes = mount({ '/': route({ title: "Hats 'n' Flamethrowers 'r' Us", getData: (request) => { console.log('headers', request.headers) return api.fetchProducts() }, view: <Landing />, status: 200, }), '/product': lazy(() => import('./product')), }) // Creates a "navigation" object, which contains the same functionality // as the browser `<Router>` object, but handled in-memory. const navigation = createMemoryNavigation({ routes, request: { headers: { 'authorization-token': '123', }, url: window.location.pathname } }) // Wait for the navigation object to stabilise, so it can be rendered // immediately. await navigation.steady() console.log('status', navigation.getCurrentValue().status) // As `renderToString()` doesn't support `<Suspense>`, instead of using // a `<Router>` with a `<Suspense>`, you'll need to manually pass your // navigation object to a `<NaviProvider>` component. let html = ReactDOMServer.renderToString( <NaviProvider navigation={navigation}> <Layout> <View /> </Layout> </NaviProvider> ) document.body.innerHTML = html } main()
As of right now, there’s no official package for integrating Navi with Express. There should be one. Please make one. I’ll tell the world about it.
But seriously, I will tell the world about anything you make with Navi. Here’s how I’ll do it: the Frontend Armory weekly newsletter. Want to hear about awesome React shit that other readers are making? Join it. You know you want to.
Join Frontend Armory for Free»
But that’s it from me today. Thanks so much for reading. I’ve poured my heart into this project because I believe that it’ll make your life as a React developer so much easier. Now go build something amazing!
P.S. I haven’t forgotten about the React in Practice course — I’m working on it right now, and it’ll include hooks. I’ll have more more details real soon.
Recommend
-
70
-
32
Building a Polyfill for React Suspense I chose this image for no other reason than be...
-
60
react 16给我们带来的一些列重要变化 render /纯组件能够 return 任何数据结构,以及 CreatePortal Api 新的 context api ,尝试代替一部分...
-
17
There has been plenty of interest in React Suspense with many articles and experimental code snippets to test it out. I thought I would read more about it and give my understanding of why you might want to use it. Below i...
-
28
Understanding React’s experimental Concurrent and Suspense.
-
12
React Suspense and Error Boundary TLDR: Suspend can catch Promise from children and render fallback until the pro...
-
19
In this article, we are go to build a simple profile app with a combination of React.lazy(), Suspense and ErrorBoundary component. Don’t forget to check the complete working codes in the end of this page.
-
16
(This post is also on medium) Why Hooks Stateful logic is unavoidable in any React project. In early days we used the st...
-
6
Not FoundYou just hit a route that doesn't exist... the sadness.LoginRadius empowers businesses to deliver a delightful customer experience and win customer trust. Using the LoginRadius Identity...
-
3
Suspense in React 18: How it works, and how you can use it React 18, also known as the Concurrent React, released earlier this year and brought with it important changes. The most i...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK