Solid Router brings fine-grained reactivity to route navigation, enabling your single-page application to become multi-paged without full page reloads. Fully integrated into the SolidJS ecosystem, Solid Router provides declarative syntax with features like universal rendering and parallel data fetching for best performance.
Explore the official documentation for detailed guides and examples.
Core Features
- All Routing Modes:
- History-Based for standard browser navigation
- Hash-Based for navigation based on URL hash
- Static Routing for server-side rendering (SSR)
- Memory-Based for testing in non-browser environments
- TypeScript: Full integration for robust, type-safe development
- Universal Rendering: Seamless rendering on both client and server environments
- Declarative: Define routes as components or as an object
- Preload Functions: Parallel data fetching, following the render-as-you-fetch pattern
- Dynamic Route Parameters: Flexible URL patterns with parameters, optional segments, and wildcards
- Data APIs with Caching: Reactive data fetching with deduplication and revalidation
Table of contents
- Getting Started
- Dynamic Routes
- Nested Routes
- Hash Mode Router
- Memory Mode Router
- Data APIs
- Config Based Routing
- Components
- Router Primitives
- SPAs in Deployed Environments
Getting Started
Set Up the Router
# use preferred package manager
npm add @solidjs/routerInstall @solidjs/router, then start your application by rendering the router component
import { render } from "solid-js/web"; import { Router } from "@solidjs/router"; render(() => <Router />, document.getElementById("app"));
This sets up a Router that will match on the url to display the desired page
Configure Your Routes
Solid Router allows you to configure your routes using JSX:
- Add each route to a
<Router>using theRoutecomponent, specifying a path and a component to render when the user navigates to that path.
import { render } from "solid-js/web"; import { Router, Route } from "@solidjs/router"; import Home from "./pages/Home"; import Users from "./pages/Users"; render( () => ( <Router> <Route path="/users" component={Users} /> <Route path="/" component={Home} /> </Router> ), document.getElementById("app") );
- Provide a root level layout
This will always be there and won't update on page change. It is the ideal place to put top level navigation and Context Providers
import { render } from "solid-js/web"; import { Router, Route } from "@solidjs/router"; import Home from "./pages/Home"; import Users from "./pages/Users"; const App = (props) => ( <> <h1>My Site with lots of pages</h1> {props.children} </> ); render( () => ( <Router root={App}> <Route path="/users" component={Users} /> <Route path="/" component={Home} /> </Router> ), document.getElementById("app") );
- Create a catch-all route (404 page)
We can create catch-all routes for pages not found at any nested level of the router. We use * and optionally the name of a parameter to retrieve the rest of the path.
import { render } from "solid-js/web"; import { Router, Route } from "@solidjs/router"; import Home from "./pages/Home"; import Users from "./pages/Users"; import NotFound from "./pages/404"; const App = (props) => ( <> <h1>My Site with lots of pages</h1> {props.children} </> ); render( () => ( <Router root={App}> <Route path="/users" component={Users} /> <Route path="/" component={Home} /> <Route path="*404" component={NotFound} /> </Router> ), document.getElementById("app") );
- Lazy-load route components
This way, the Users and Home components will only be loaded if you're navigating to /users or /, respectively.
import { lazy } from "solid-js"; import { render } from "solid-js/web"; import { Router, Route } from "@solidjs/router"; const Users = lazy(() => import("./pages/Users")); const Home = lazy(() => import("./pages/Home")); const App = (props) => ( <> <h1>My Site with lots of pages</h1> {props.children} </> ); render( () => ( <Router root={App}> <Route path="/users" component={Users} /> <Route path="/" component={Home} /> </Router> ), document.getElementById("app") );
Create Links to Your Routes
Use an anchor tag that takes you to a route:
import { lazy } from "solid-js"; import { render } from "solid-js/web"; import { Router, Route } from "@solidjs/router"; const Users = lazy(() => import("./pages/Users")); const Home = lazy(() => import("./pages/Home")); const App = (props) => ( <> <nav> <a href="/about">About</a> <a href="/">Home</a> </nav> <h1>My Site with lots of pages</h1> {props.children} </> ); render( () => ( <Router root={App}> <Route path="/users" component={Users} /> <Route path="/" component={Home} /> </Router> ), document.getElementById("app") );
Dynamic Routes
If you don't know the path ahead of time, you might want to treat part of the path as a flexible parameter that is passed on to the component.
import { lazy } from "solid-js"; import { render } from "solid-js/web"; import { Router, Route } from "@solidjs/router"; const Users = lazy(() => import("./pages/Users")); const User = lazy(() => import("./pages/User")); const Home = lazy(() => import("./pages/Home")); render( () => ( <Router> <Route path="/users" component={Users} /> <Route path="/users/:id" component={User} /> <Route path="/" component={Home} /> </Router> ), document.getElementById("app") );
The colon indicates that id can be any string, and as long as the URL fits that pattern, the User component will show.
You can then access that id from within a route component with useParams.
Note on Animation/Transitions:
Routes that share the same path match will be treated as the same route. If you want to force re-render you can wrap your component in a keyed <Show> like:
<Show when={params.something} keyed> <MyComponent /> </Show>
Each path parameter can be validated using a MatchFilter.
This allows for more complex routing descriptions than just checking the presence of a parameter.
import { lazy } from "solid-js"; import { render } from "solid-js/web"; import { Router, Route } from "@solidjs/router"; import type { MatchFilters } from "@solidjs/router"; const User = lazy(() => import("./pages/User")); const filters: MatchFilters = { parent: ["mom", "dad"], // allow enum values id: /^\d+$/, // only allow numbers withHtmlExtension: (v: string) => v.length > 5 && v.endsWith(".html"), // we want an `*.html` extension }; render( () => ( <Router> <Route path="/users/:parent/:id/:withHtmlExtension" component={User} matchFilters={filters} /> </Router> ), document.getElementById("app") );
Here, we have added the matchFilters prop. This allows us to validate the parent, id and withHtmlExtension parameters against the filters defined in filters.
If the validation fails, the route will not match.
So in this example:
/users/mom/123/contact.htmlwould match,/users/dad/123/about.htmlwould match,/users/aunt/123/contact.htmlwould not match as:parentis not 'mom' or 'dad',/users/mom/me/contact.htmlwould not match as:idis not a number,/users/dad/123/contactwould not match as:withHtmlExtensionis missing.html.
Optional Parameters
Parameters can be specified as optional by adding a question mark to the end of the parameter name:
// Matches stories and stories/123 but not stories/123/comments <Route path="/stories/:id?" component={Stories} />
Wildcard Routes
:param lets you match an arbitrary name at that point in the path. You can use * to match any end of the path:
// Matches any path that begins with foo, including foo/, foo/a/, foo/a/b/c <Route path="foo/*" component={Foo} />
If you want to expose the wild part of the path to the component as a parameter, you can name it:
<Route path="foo/*any" component={Foo} />
Note that the wildcard token must be the last part of the path; foo/*any/bar won't create any routes.
Multiple Paths
Routes also support defining multiple paths using an array. This allows a route to remain mounted and not rerender when switching between two or more locations that it matches:
// Navigating from login to register does not cause the Login component to re-render <Route path={["login", "register"]} component={Login} />
Nested Routes
The following two route definitions have the same result:
<Route path="/users/:id" component={User} />
<Route path="/users"> <Route path="/:id" component={User} /> </Route>
/users/:id renders the <User/> component, and /users/ is an empty route.
Only leaf Route nodes (innermost Route components) are given a route. If you want to make the parent its own route, you have to specify it separately:
//This won't work the way you'd expect <Route path="/users" component={Users}> <Route path="/:id" component={User} /> </Route> // This works <Route path="/users" component={Users} /> <Route path="/users/:id" component={User} /> // This also works <Route path="/users"> <Route path="/" component={Users} /> <Route path="/:id" component={User} /> </Route>
You can also take advantage of nesting by using props.children passed to the route component.
function PageWrapper(props) { return ( <div> <h1> We love our users! </h1> {props.children} <A href="/">Back Home</A> </div> ); } <Route path="/users" component={PageWrapper}> <Route path="/" component={Users} /> <Route path="/:id" component={User} /> </Route>;
The routes are still configured the same, but now the route elements will appear inside the parent element where the props.children was declared.
You can nest indefinitely - just remember that only leaf nodes will become their own routes. In this example, the only route created is /layer1/layer2, and it appears as three nested divs.
<Route path="/" component={(props) => <div>Onion starts here {props.children}</div>} > <Route path="layer1" component={(props) => <div>Another layer {props.children}</div>} > <Route path="layer2" component={() => <div>Innermost layer</div>} /> </Route> </Route>
Preload Functions
Even with smart caches it is possible that we have waterfalls both with view logic and with lazy loaded code. With preload functions, we can instead start fetching the data parallel to loading the route, so we can use the data as soon as possible. The preload function is called when the Route is loaded or eagerly when links are hovered.
As its only argument, the preload function is passed an object that you can use to access route information:
import { lazy } from "solid-js"; import { Route } from "@solidjs/router"; const User = lazy(() => import("./pages/users/[id].js")); // preload function function preloadUser({ params, location }) { // do preloading } // Pass it in the route definition <Route path="/users/:id" component={User} preload={preloadUser} />;
| key | type | description |
|---|---|---|
| params | object | The route parameters (same value as calling useParams() inside the route component) |
| location | { pathname, search, hash, query, state, key} |
An object that you can use to get more information about the path (corresponds to useLocation()) |
| intent | "initial", "navigate", "native", "preload" |
Indicates why this function is being called.
|
A common pattern is to export the preload function and data wrappers that corresponds to a route in a dedicated route.data.js file. This way, the data function can be imported without loading anything else.
import { lazy } from "solid-js"; import { Route } from "@solidjs/router"; import preloadUser from "./pages/users/[id].data.js"; const User = lazy(() => import("/pages/users/[id].js")); // In the Route definition <Route path="/users/:id" component={User} preload={preloadUser} />;
The preload function's return value is passed to the page component for any intent other than "preload", allowing you to initialize data or alternatively use our new Data APIs:
Data APIs
Keep in mind that these are entirely optional, but they demonstrate the power of our preload mechanism.
query
To prevent duplicate fetching and to handle refetching triggers, we provide a query API that accepts a function and returns the same function.
const getUser = query(async (id) => { return (await fetch(`/api/users/${id}`)).json(); }, "users"); // used as the query key + serialized arguments
It is expected that the arguments to the query function are serializable.
This query accomplishes the following:
- It does deduping on the server for the lifetime of the request.
- It fills a preload cache in the browser which lasts 5 seconds. When a route is preloaded on hover or when preload is called when entering a route it will make sure to dedupe calls.
- We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation.
- It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses this cache. Revalidation or new fetch updates the cache.
Using it with preload function might look like:
import { lazy } from "solid-js"; import { Route } from "@solidjs/router"; import { getUser } from ... // the query function const User = lazy(() => import("./pages/users/[id].js")); // preload function function preloadUser({params, location}) { void getUser(params.id) } // Pass it in the route definition <Route path="/users/:id" component={User} preload={preloadUser} />;
Inside your page component you:
// pages/users/[id].js import { getUser } from ... // the query function export default function User(props) { const user = createAsync(() => getUser(props.params.id)); return <h1>{user().name}</h1>; }
Cached function has a few useful methods for getting the key that are useful for invalidation.
let id = 5; getUser.key; // returns "users" getUser.keyFor(id); // returns "users[5]"
You can revalidate the query using the revalidate method or you can set revalidate keys on your response from your actions. If you pass the whole key it will invalidate all the entries for the query (ie "users" in the example above). You can also invalidate a single entry by using keyFor.
query can be defined anywhere and then used inside your components with:
createAsync
This is light wrapper over createResource that aims to serve as stand-in for a future primitive we intend to bring to Solid core in 2.0. It is a simpler async primitive where the function tracks like createMemo and it expects a promise back that it turns into a Signal. Reading it before it is ready causes Suspense/Transitions to trigger.
const user = createAsync((currentValue) => getUser(params.id));
It also preserves latest field from createResource. Note that it will be removed in the future.
const user = createAsync((currentValue) => getUser(params.id)); return <h1>{user.latest.name}</h1>;
Using query in createResource directly won't work properly as the fetcher is not reactive and it won't invalidate properly.
createAsyncStore
Similar to createAsync except it uses a deeply reactive store. Perfect for applying fine-grained changes to large model data that updates.
It also supports latest field which will be removed in the future.
const todos = createAsyncStore(() => getTodos());
action
Actions are data mutations that can trigger invalidations and further routing. A list of prebuilt response helpers can be found below.
const deleteTodo = action(async (formData: FormData) => { const id = Number(formData.get("id")) await api.deleteTodo(id) }) <form action={deleteTodo} method="post"> <input type="hidden" name="id" value={todo.id} /> <button type="submit">Delete</button> </form>
Instead with with you can write this:
const deleteTodo = action(api.deleteTodo) <form action={deleteTodo.with(todo.id)} method="post"> <button type="submit">Delete</button> </form>
Actions also take a second argument which can be the name or an option object with name and onComplete. name is used to identify SSR actions that aren't server functions (see note below). onComplete allows you to configure behavior when actions complete. Keep in mind onComplete does not work when JavaScript is disabled.