NDREAN
Posted on July 14, 2022
A quick showcase of UniversalRouter (1.6kB zip) as an "easy" alternative to React Router.
What is it? The code used for the router is not embedded within React components. It is pure Javascript code that allows executing code on a given path, and then delivers React components to React for rendering. It uses the browser navigation and the History interface.
What is the point of using this? An example: when you navigate to a page, you may want to render data. Instead of using a useEffect
in the component, you can pre-fetch the data and then pass it as an argument to a stateless React component, all this asynchronously (and no double rendering).
Lastly, the code is pretty stable :)
In this showcase, we don't use redirects, just a "nabvar" component with links that stays on top of each page and renders components as children
.
Map of "links"
A navbar is a collection of links. Each link has a path
and title
attribute. We define a map of objects that contain these attributes:
export const useLinks = [
{ path: "/", title: "Home" },
{ path: "/users", title: "pre-render" },
{ path: "/vusers", title: "Valtio store" },
[...]
];
Each object of this array will be the arguments of a "link" element.
const Link = ({ path, title, handler }) =>
<a href={path} onClick={handler}>
{title}
</a>
The onClick
handler is defined in the parent component "Navbar". If any extra code needs to be executed for a given path, we can define it in our route array, as seen further down.
The Navbar
We build the Navbar component by iterating over the map of <Link />
objects.
The "onClick" handler will simply push the found pathname attribute into the browser history session. The Navbar will render any component as a child.
const NavBar = ({ children }) => {
function handleNav(e) {
e.preventDefault();
history.push({ pathname: e.target.pathname });
}
return (
<>
{useLinks.map(({ path, title }) => (
<Link key={title} path={path} title={title} handler={handleNav} />
))}
{children}
</>
);
};
The routes
The routing is made by defining a route
object which is an array of path
objects. A path object is defined with three reserved keys: path
, action
and children
. A path object has the form:
{path: "/component", action: handler, children: []}
The optional children
array will give nested routes.
An action
is simply a function. It will eventually return a React component (in our case as we have no redirects). Every action
can be made async, in particular, we can make dynamic imports.
For example, suppose we want to navigate to a page which displays data retrieved from an api. The action can be:
async function PreFetch() {
const users = await fetchComments(8);
const { default: Users } = await import("../utils/users");
return <Users data={users} />;
}
and our path object is:
{ path: "/users", action: async () => PreFetch() }
It admits an object context
that can be used by the path objects. The action
accepts the context
object from the routes as an attribute. We can use this to pass a data store for example (we showcased a Valtio data store here) so that we don't need to spread the store through the code. Just inject it into the component through the routes. Easy!
The context object also captures the "params" if needed.
An example of a route array that UniversalRouter will transverse:
const routes = [
{
// wrapping the routes with the Navbar and render every component as a child
path: "",
action: async ({ next }) => {
const component = await next();
const { default: NavBar} = await import('./NavBar')
return component && <NavBar>{component}</NavBar>
},
children: [
{
path: "/",
action: async () =>
import(".Home").then(({ Home }) => <Home />)
},
{
path: "/users",
action: async () => PreFetch()
},
{
path: "/vusers",
async action({ vStore }) {
await vStore.getUsers(2);
const { default: Users } = await import("../utils/users");
return <Users data={vStore.users} />;
}
},
{
path: "(.*)",
action: () => <img scr="404.webp" ...</h1>
}
]
}
];
const Users = ({ data }) => (
<>{data && data.map((user) => <User key={..}... />)}</>
);
Route parsing and rendering
- We initiate the browser history session as:
import { createBrowserHistory } from "history";
export default createBrowserHistory();
- We initiate our
router
object and can pass in somecontext
whenever anaction
might need it. For example, we use a data store (calledvStore
here) managed by Valtio:
// example of Valtio store
import { proxy, useSnapshot } from "valtio";
import { fetchUsers } from "./fetchUsers";
export { useSnapshot };
export const vStore = proxy({
users: null,
async getUsers(id) {
vStore.users = await fetchUsers(id);
},
});
We can pass it to the context
key in the constructor and any path object action
method can use this store with action(context) {...}
whenever needed.
const router = new UniversalRouter(routes, {context: {vStore}});
How does this work?
The history
listens to paths change and triggers a renderRoute
function. UniversalRouter transverses the "routes" array used in the constructor to find a match with the path. It then executes the action which will return a React component (in our case). It then calls the React.render
function on the returned function.
import { createRoot } from "react-dom/client";
import React from "react";
import UniversalRouter from "universal-router";
import history from "./router/history";
import routes from "./router/routes";
import { vStore } from "./valtio/vStore";
const context = { vStore };
const router = new UniversalRouter(routes, { context });
const root = createRoot(document.getElementById("root"));
async function renderRoute(location) {
try {
// "history" returns a path, and "router" finds a match in the routes array
const page = await router.resolve({
pathname: location.pathname
});
return root.render(<>{page}</>);
} catch (err) {
console.log(err);
return root.render(<p>Wrong!</p>);
}
}
history.listen(({ location }) => renderRoute(location));
history.push("/");
A word about redirection
Just return a path that returns an object {redirect: "/path_you_want"}
. With the middleware approach and our Navbar, we modify the path:
path: "",
action: async ({ next }) => {
const component = await next();
const { default: NavBar} = await import('./NavBar')
if (component.redirect) return component
return component && <NavBar>{component}</NavBar>
},
Since "component" will contain the key "redirect", it remains to let the resolve
function return the path in the "main.js". This works thanks to the key "redirect":
const page = await router.resolve({
pathname: location.pathname
});
if (page.redirect) {
return history.push(page.redirect);
}
return root.render(<>{page}</>);
Posted on July 14, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 8, 2024