React Router to ShadowJS
Port React Router and Remix navigation to @shadow-js/router with a codemod for links, the outlet, redirects and the navigation hooks, and a map from route objects, loaders and the router provider to defineRoute and createRouter.
React Router and @shadow-js/router solve the same problem with different shapes. Links, the outlet and the navigation hooks
map almost one to one; the route table becomes defineRoute objects; loaders keep their idea (data before render) and gain
types from the path. This page is the router part of a port: components, hooks and state are in
React to ShadowJS, and file routes with a server are Dusk.
Automate it#
create-shadow-app codemod react-router-to-shadow rewrites what maps without judgment, for imports from react-router,
react-router-dom and @remix-run/react: Link and NavLink become A, and Outlet, Navigate, useNavigate, useLocation
and useParams move to @shadow-js/router. It parses each file, edits by source range and prints what it left for you. Run it
without a flag to see a diff and write nothing, then again with --write. A second run changes nothing. See
Migrate code with codemods for the options and the exit codes.
npx create-shadow-app codemod react-router-to-shadow src
npx create-shadow-app codemod react-router-to-shadow src --write
| React Router | The codemod writes | Why |
|---|---|---|
<Link to="/x">, <NavLink to="/x"> |
<A to="/x"> |
NavLink's active state is activeClass (and end) on A |
className="x" on a link |
class="x" |
A joins class with activeClass |
prefetch="intent" (or viewport, render, none) |
preload="intent" |
The same four strategies; render and none need prefetch() |
activeClassName (v5) |
activeClass |
|
Outlet, Navigate, useLocation |
the same names from @shadow-js/router |
|
useNavigate, useParams |
the same names from @shadow-js/router |
useParams returns a view; a note says to read it where it is used |
It refuses, with the file:line and the reason, and leaves the import alone: a to that is an object ({ pathname, search }),
className or style written as a function of { isActive }, reloadDocument, preventScrollReset, target, download,
discover, a spread of props on a link, and navigate(-1) (a number moves through history; write useRouter().history.back()).
Everything else from the React Router packages is reported with what to write instead. This is the exact output for the input shown:
import { Link, NavLink, Navigate, Outlet, useLocation, useNavigate, useParams } from "react-router-dom";
export function Shell() {
const navigate = useNavigate();
const location = useLocation();
const { id } = useParams();
return (
<div>
<nav>
<Link to="/" className="brand" prefetch="intent">
Home
</Link>
<NavLink to="/posts" end activeClassName="current">
Posts
</NavLink>
<a href="/about">About</a>
</nav>
<button onClick={() => navigate("/posts/new", { replace: true })}>New</button>
<p>
{location.pathname} {id}
</p>
{id === undefined ? <Navigate to="/posts" replace /> : <Outlet />}
</div>
);
}
import { A, Navigate, Outlet, useLocation, useNavigate, useParams } from "@shadow-js/router";
export function Shell() {
const navigate = useNavigate();
const location = useLocation();
const { id } = useParams();
return (
<div>
<nav>
<A to="/" class="brand" preload="intent">
Home
</A>
<A to="/posts" end activeClass="current">
Posts
</A>
<a href="/about">About</a>
</nav>
<button onClick={() => navigate("/posts/new", { replace: true })}>New</button>
<p>
{location.pathname} {id}
</p>
{id === undefined ? <Navigate to="/posts" replace /> : <Outlet />}
</div>
);
}
What you write by hand#
The route table#
<BrowserRouter>, <Routes>, <Route>, createBrowserRouter and createRoutesFromElements become defineRoute objects and one
createRouter. A route's element is its component; path keeps its :param syntax; nesting is children, and <Outlet />
renders the child.
import { createRouter, defineRoute, Outlet, RouterProvider } from "@shadow-js/router";
import type { JSX } from "@shadow-js/web";
const posts = defineRoute("/posts", {
component: () => (
<section>
<h1>Posts</h1>
<Outlet />
</section>
),
children: [
defineRoute("/:id", { component: () => <article>One post</article> }),
],
});
const home = defineRoute("/", { component: () => <h1>Home</h1> });
export const router = createRouter({ routes: [home, posts] });
export function App(): JSX.Element {
return <RouterProvider router={router} />;
}
Loaders#
A React Router loader returns a value that useLoaderData() reads. In ShadowJS the loader gets { params, search, context, signal }
and useLoaderData() returns an accessor: read the data with (). A loader that throws redirect(...) or notFound() works
as in React Router, with the helpers from @shadow-js/router. Loaders run in parallel for the whole chain of matched routes,
beforeLoad runs top-down first, and the lazy code of each route is requested together with its data, so the cost of a navigation is the slower of the two.
import { defineRoute, useLoaderData } from "@shadow-js/router";
interface Post {
title: string;
}
function PostPage() {
const post = useLoaderData<Post>();
return <h1>{post()?.title}</h1>;
}
export const post = defineRoute("/posts/:id", {
loader: ({ params }): Post => ({ title: `Post ${params.id}` }),
component: PostPage,
});
Search params, forms and actions#
useSearchParams becomes useSearch() for validated values (give the route a validateSearch) or useLocation().search for the
raw string. React Router's <Form> and actions, and Remix's useFetcher, correspond to server functions
and <Form>, useFetcher and useSubmit of @shadow-js/dusk/client; see Dusk: mutations.
Scroll restoration and view transitions#
<ScrollRestoration /> is the scroll() feature of @shadow-js/router/scroll. unstable_viewTransition is the viewTransitions()
feature (@shadow-js/router/transitions), with viewTransition={false} to opt a link out.
See also#
- Routing: the full guide to
@shadow-js/router. - React to ShadowJS: components, hooks and state.
- Next.js to ShadowJS: the same port for the Next.js router.