Migrating To Archibald Router
ArchibaldRouter introduces several powerful new features, as well as improved compatibility with the latest versions of React. It also introduces a few breaking changes from React Router v5. This document is a comprehensive guide on how to upgrade your ReactRouter-based app to ArchibaldRouter.
The examples in this guide will show code samples of how you might have built something in a ReactRouter app, followed by how you would accomplish the same thing in ArchibaldRouter. There will also be an explanation of why we made this change and how it's going to improve both your code and the overall user experience of people who are using your app.
Upgrade all <Switch /> elements to <Routes />
Archibald Router introduces a Routes component that is kind of like Switch, but a lot more powerful. The main advantages of Routes over Switch are:
- All
<Route>s and<RouterLink>s inside a<Routes>are relative. This leads to leaner and more predictable code in<Route path>and<RouterLink to> - Routes are chosen based on the best match instead of being traversed in order. This avoids bugs due to unreachable routes because they were defined later in your
<Switch> - Routes may be nested in one place instead of being spread out in different components. In small to medium-sized apps, this lets you easily see all your routes at once. In large apps, you can still nest routes in bundles that you load dynamically via
lazy
Upgrade your <Route />s
Instead of using <Route component> and <Route render> props, just use regular element <Route element> everywhere and use hooks to access the router's internal state.
// ReactRouter v5
function User({ id }) {
// ...
}
function App() {
return (
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/users/:id" render={({ match }) => <User id={match.params.id} />} />
</Switch>
);
}
// ArchibaldRouter style
function User() {
let { id } = useParams();
// ...
}
function App() {
return (
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<User />} />
</Routes>
);
}
When you use regular React elements you get to pass the props explicitly. This helps with code readability and maintenance over time. If you were using <Route render={}> to get a hold of the params, you can just useParams inside your route component instead.
Refactor custom <Route>s
Replace any elements inside a <Routes> that are not plain <Route> elements with a regular <Route>. This includes any <PrivateRoute>-style custom components.
You can read more about the rationale behind this here.
Relative Routes and Links
In ReactRouter v5, you had to be very explicit about how you wanted to nest your routes and links. In both cases, if you wanted nested routes and links you had to build the <Route path> and <RouterLink to> props from the parent route's match.url and match.path properties. Additionally, if you wanted to nest routes, you had to put them in the child route's component.
// This is a React Router v5 app
import { BrowserRouter, Link, Route, Switch, useRouteMatch } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/users">
<Users />
</Route>
</Switch>
</BrowserRouter>
);
}
function Users() {
// Nested routes are rendered by the child component, so
// you have <Switch> elements all over your app for nested UI.
// You build nested routes and links using match.url and match.path. let match = useRouteMatch();
return (
<div>
<nav>
<RouterLink to={`${match.url}/me`}>My Profile</RouterLink>
</nav>
<Switch>
<Route path={`${match.path}/me`}>
<OwnUserProfile />
</Route>
<Route path={`${match.path}/:id`}>
<UserProfile />
</Route>
</Switch>
</div>
);
}
This is the same app in ArchibaldRouter:
// This is an ArchibaldRouter app
import { Route, RouterLink, Routes, SuspenseRouter } from '@archibald/core';
function App() {
return (
<SuspenseRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users/*" element={<Users />} />
</Routes>
</SuspenseRouter>
);
}
function Users() {
return (
<div>
<nav>
<RouterLink to="me">My Profile</RouterLink>
</nav>
<Routes>
<Route path=":id" element={<UserProfile />} />
<Route path="me" element={<OwnUserProfile />} />
</Routes>
</div>
);
}
A few important things to notice about v6 in this example:
<Route path>and<RouterLink to>are relative. This means that they automatically build on the parent route's path and URL so you don't have to manually interpolatematch.urlormatch.path<Route exact>is gone. Instead, routes with descendant routes (defined in other components) use a trailing*in their path to indicate they match deeply- You may put your routes in whatever order you wish and the router will automatically detect the best route for the current URL. This prevents bugs due to manually putting routes in the wrong order in a
<Switch>
Advantages of <Route element>
For starters, we see React itself taking the lead here with the <Suspense fallback={<Spinner />}> API. The fallback prop takes a React element, not a component. This lets you easily pass whatever props you want to your <Spinner> from the component that renders it.
Also, in case you didn't notice, in ReactRouter v5 Route's rendering API became rather large. It went something like this:
// Ah, this is nice and simple!
<Route path=":userId" component={Profile} />;
// But wait, how do I pass custom props to the <Profile> element??
// Hmm, maybe we can use a render prop in those situations?
<Route path=":userId" render={(routeProps) => <Profile routeProps={routeProps} animate={true} />} />;
// Ok, now we have two ways to render something with a route. :/
// But wait, what if we want to render something when a route
// *doesn't* match the URL, like a Not Found page? Maybe we
// can use another render prop with slightly different semantics?
<Route path=":userId" children={({ match }) => (match ? <Profile match={match} animate={true} /> : <NotFound />)} />;
Now, the conversation above goes like this:
// Ah, nice and simple API. And it's just like the <Suspense> API!
// Nothing more to learn here.
<Route path=":userId" element={<Profile />} />
// But wait, how do I pass custom props to the <Profile>
// element? Oh ya, it's just an element. Easy.
<Route path=":userId" element={<Profile animate={true} />} />
// Ok, but how do I access the router's data, like the URL params
// or the current location?
function Profile({ animate }) {
const params = useParams();
const location = useLocation();
}
Another important reason for using the element prop is that <Route children> is reserved for nesting routes. Taking the code in the previous example one step further, we can hoist all <Route> elements into a single route config:
import { Outlet, Route, RouterLink, Routes, SuspenseRouter } from '@archibald/core';
function App() {
return (
<SuspenseRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<Users />}>
<Route path="me" element={<OwnUserProfile />} />
<Route path=":id" element={<UserProfile />} />
</Route>
</Routes>
</SuspenseRouter>
);
}
function Users() {
return (
<div>
<nav>
<RouterLink to="me">My Profile</RouterLink>
</nav>
<Outlet />
</div>
);
}
Notice how <Route> elements nest naturally inside a <Routes> element. Nested routes build their path by adding to the parent route's path. We didn't need a trailing * on <Route path="users"> this time because when the routes are defined in one spot the router is able to see all your nested routes.
You'll only need the trailing * when there is another <Routes> somewhere in that route's descendant tree. In that case, the descendant <Routes> will match on the portion of the pathname that remains (see the previous example for what this looks like in practice).
When using a nested config, routes with children should render an <Outlet> in order to render their child routes. This makes it easy to render layouts with nested UI.
Note on <Route path> patterns
Archibald Router uses a fully charged URLPattern Web API format. See:
For the Browsers that doesn't support it yet, we have a polyfill enabled.
Use useNavigate instead of useHistory
ArchibaldRouter introduces a new navigation API that is synonymous with <RouterLink> and provides better compatibility with suspense-enabled apps. We include both imperative and declarative versions of this API depending on your style and needs.
// This is a React Router v5 app
import { useHistory } from 'react-router-dom';
function App() {
const history = useHistory();
function handleClick() {
history.push('/home');
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}
In ArchibaldRouter, this app should be rewritten to use the navigate API. Most of the time this means changing useHistory to useNavigate and changing the history.push or history.replace callsite.
// This is an ArchibaldRouter app
import { useNavigate } from '@archibald/core';
function App() {
const navigate = useNavigate();
function handleClick() {
navigate('/home');
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}
If you need to replace the current location instead of push a new one onto the history stack, use navigate(to, { replace: true }). If you need state, use navigate(to, { state }). You can think of the first argument to navigate as your <RouterLink to> and the other arguments as the replace and state props.
If you prefer to use a declarative API for navigation (ala v5's Redirect component), v6 provides a Navigate component. Use it like:
import { Navigate } from '@archibald/core';
function App() {
return <Navigate to="/home" replace state={state} />;
}
If you're currently using go, goBack or goForward from useHistory to navigate backwards and forwards, you should also replace these with navigate with a numerical argument indicating where to move the pointer in the history stack.
// This is a React Router v5 app
import { useHistory } from 'react-router-dom';
function App() {
const { go, goBack, goForward } = useHistory();
return (
<>
<button onClick={() => go(-2)}>Go 2 pages back</button>
<button onClick={goBack}>Go back</button>
<button onClick={goForward}>Go forward</button>
<button onClick={() => go(2)}>Go 2 pages forward</button>
</>
);
}
Here is the equivalent app in Archibald project:
// This is an ArchibaldRouter app
import { useNavigate } from '@archibald/core';
function App() {
const navigate = useNavigate();
return (
<>
<button onClick={() => navigate(-2)}>Go 2 pages back</button>
<button onClick={() => navigate(-1)}>Go back</button>
<button onClick={() => navigate(1)}>Go forward</button>
<button onClick={() => navigate(2)}>Go 2 pages forward</button>
</>
);
}
Language fix
Language fix is moved from a useFixLanguagePath hook to ArchibaldRouter internals.
All you need to do is to enable/disable it by provided fixLanguage flag in your environment configuration file and delete usage of the hook.
Last reminder
Double-check that all your imports of Router hooks/components are currently done from @archibald/core instead of react-router or react-router-dom. You can safely remove external package now.💪
Update: there is a good chance that some of your imports like useSSR etc have been moved form any archibald package they've been before to @archibald/core. Please run lint and adapt those imports accordingly as well.
Moved to @archibald/core:
URLHelperfrom @archibald/clientuseLanguagefrom @archibald/storefrontuseSSRfrom @archibald/client
useParams types
There is currently a known limitation with useParams: you cannot define and provide interface for it :(. Please use type instead.
// does not work:
interface BadParams {
language: string;
}
const badParams = useParams<BadParams>();
// works:
type GoodParams = {
language: string;
};
const goodParams = useParams<GoodParams>();
const inlineParams = useParams<{ language: string }>();
Moving fetching logic from root component to the first layout
It's advised to move any of your fetching logic (usually fetching messages etc) from your App.tsx component to the first layout that wraps your entire application.
As long as <Routes> are matching requested URL with the path patterns, fixing the language and rendering your corresponding <Route> you don't want to start fetching before you start rendering. Example, you have missing/incorrect language in the requested URL, you start fetching the messages (wrong data is fetched, code execution is delayed) then you start matching <Route>s, ArchibaldRouter fixes your incorrect * *language**, redirects and cycle repeats.
// previously
function App() {
useMessages();
useInitApp();
return <ShopRoutes />;
}
// In ArchibaldRouter
function App() {
return <ShopRoutes />;
}
function ShopRoutes() {
return (
<Routes>
<Route path="/:language?" element={<Layout />}>
{/* further application routes */}
</Route>
</Routes>
);
}
// fetching will not start until there is a match and ArchibaldRouter starts rendering the corresponding Route
function Layout() {
useMessages();
useInitApp();
return <Outlet />;
}
What did we miss?
Despite our best attempts at being thorough, it's very likely that we missed something. If you follow this upgrade guide and find that to be the case, please let us know. We are happy to help you figure out what to do with your ReactRouter code to be able to upgrade and take advantage of all of the cool stuff in ArchibaldRouter.
Good luck 🤘