A React Router with Hooks and Suspense


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')
)

#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.

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.

terrible looking loading

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.

loading indicator with no delay

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.

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.

#More neat tricks

I’m not going to drop the entire set of guides, 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:

#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:

You can also check out the examples directory of the Navi repository for full code examples, including:

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 called Chunks — 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() and route() are just Generator Functions. In fact, it’s entirely possible to create custom matchers. For instance, you could create a withTimeout() 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 ❤️

  • If you’d like to see this project grow, please give it a 🌟Star on Github!

#One more thing…

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 through Navi vs. Next.js. Or if you just want the server rendering demo:

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.