React-like solid.js browser router with hassle-free nesting / dynamic routes
Solid.js default solid-app-router package does not support convenient work with route nesting / dynamic routes. Docs says, it only supports <Outlet/>
rendering, i.e.:
<Route path='/' element={<div>Onion starts here <Outlet /></div>}>
<Route path='layer1' element={<div>Another layer <Outlet /></div>}>
<Route path='layer2' element={<div>Innermost layer</div>}></Route>
</Route>
</Route>
So, as you can see, it's not really convenient and component/reactive way. And I've decided to write a better lib, i.e.:
<Routes>
<Route path={'/'}>
<div>Onion starts here <Onion /></div>
</Route>
</Routes>
// for /*
const Onion = () => (
<Routes>
<Route path={'layer1'}>
<div>Another layer <Onion2 /></div>
</Route>
</Routes>
)
// for /layer1/*
const Onion2 = () => (
<Routes>
<Route path={'layer2'}>
<div>Innermost layer</div>
</Route>
</Routes>
)
For this example the original way looks better, but if we wanna be honest, the real life case is:
You kinda coding with solid-app-router:
const App = () => (
<Router>
<Route path={'/home'} element={<HomePage/>}/>
<Route path={'/personal-account'} element={<PersonalAccountPage/>}/>
<Route path={'/'} element={<Navigate href={'/home'}/>}/>
</Router>
)
const HomePage = () => (
<>
<div>static HomePage info</div>
<Link href={'/personal-account'}>Go to personal account</Link>
</>
)
const PersonalAccountPage = () => {
const [clicks, setClicks] = createSignal(0)
const pages = createMemo(() => [
{
href: 'products',
name: 'Products',
component: ProductsComponent,
props: {clicks: clicks()}
},
{
href: 'billing',
name: 'Billing',
component: BillingComponent,
props: {clicks: clicks()},
},
])
return (
<>
<div class={'nav left-part'}>
<For each={pages()}>
{page => (
<Link href={page.href} class={'w-full'}>{page.name}</Link>
)}
</For>
<button onClick={() => setClicks(clicks() + 1)}>Click me</button>
</div>
<div class={'container right-part'}>
<For each={pages()}>
{page => (
<Route path={page.href} element={(<page.component {...page.props}/>)}/>
)}
</For>
</div>
</>
)
}
And then the problems begin, "container right-part" routing just won't work, and you will read the docs and come to a conclusion:
You MUST define routes only in a static way.
And then rewrite everything with extremely shitty <Outlet/>
strategy and lots of cross-app storages, if at all possible.
So, the only purpose of this library is to make routing workable and convenient.
- Works stably in 2023
- Any level of nesting
- No outlets, just write your code & don't worry
- Match params, parsed via regexparam
- No unnecessary mount-s
- TypeScript enabled for safety
- Without loss of reactivity
- Classical history usage
onRoute
events sharingdepsMemo
for re-renders on memo/signal change<Link>
,<Navigate>
,<DefaultRoute>
for convenient usage- Fallbacks
npm i @gh0st-work/solid-js-router
On the same example:
import {Routes, Route, Link, Router, DefaultRoute} from '@gh0st-work/solid-js-router';
import {createMemo} from "solid-js";
const App = () => (
<Router>
<Routes>
<Route path={'/home'} fallback={true}>
<HomePage/>
</Route>
<Route path={'/personal-account'}>
<PersonalAccountPage/>
</Route>
<DefaultRoute to={'/home'}/> // must be defined last, since contains "*"
</Routes>
</Router>
)
const HomePage = () => (
<>
<div>static HomePage info</div>
<Link href={'/personal-account'}>Go to personal account</Link>
</>
)
const PersonalAccountPage = () => {
const [clicks, setClicks] = createSignal(0)
const pages = createMemo(() => [
{
href: 'products',
name: 'Products',
component: ProductsComponent,
props: {clicks: clicks()} // if u pass already called signal/memo like here, u must add it to memoDeps of <Routes> or exact <Route>
},
{
href: 'billing',
name: 'Billing',
component: BillingComponent,
props: {clicks: clicks()},
},
])
return (
<>
<div class={'nav left-part'}>
<For each={pages()}>
{page => (
<Link href={page.href} class={'w-full'}>{page.name}</Link>
)}
</For>
<button onClick={() => setClicks(clicks() + 1)}>Click me</button>
</div>
<div class={'container right-part'}>
<Routes depsMemo={createMemo(() => [pages()])}>
<For each={pages()}>
{page => (
<Route path={page.href}>
<page.component {...page.props}/>
</Route>
)}
</For>
<DefaultRoute to={'products'}/> // must be defined last, since contains "*"
</Routes>
</div>
</>
)
}
And it just works perfectly.
Component for global routing management, use in only once, wrap your app in it.
Props:
- history - history package
createBrowserHistory()
instance - children - default hidden prop, your elements
Component for defining your routes, just wrap them in it.
Props:
- onRoute - function
({route, parentRoute}) => {}
that will be called on every route change - depsMemo - memo or signal getter that will on change rerender active route children (in case u wanna provide already computed signals or memo in your routes children components)
- fallback - JSX element if no available route found.
Not redirecting anywhere. - children - default hidden prop, non-
<Route>
components will be ignored
Just route component.
Props:
- path - relative path of your route.
Parsed via regexparam, so you can use matching.
Recommended starting from/
, i.e./personal-account
->/products
. - depsMemo - memo or signal getter that will on change rerender this route children (in case u wanna provide already computed signals or memo in your children components)
- fallback - boolean (
true
/false
).
If no available route found the firstfallback={true}
route will be used.
Not redirecting anywhere. - children - default hidden prop, your elements
Matching supported, child must be function:
import {Routes, Route, Link, Router, DefaultRoute, Navigate} from '@gh0st-work/solid-js-router';
const App = () => (
<Router>
<Routes>
<Route path={'/car/:id'}>
{({id}) => <Car id={id}/>}
</Route>
<Route path={'/home'}>
<Navigate to={'/car/1'}/>
</Route>
<DefaultRoute to={'/home'}/>
</Routes>
</Router>
)
const Car = ({id}) => {
return (<span>Car #{id}</span>)
}
Component that will redirect on mount.
Props:
- to - link/href to redirect
Must be defined last, since contains *
.
Just shortcut to:
<Route path={'/*'} fallback={fallback}>
<Navigate to={to}/>
</Route>
Insert it in the end of your routes and get rid of fallbacks.
Props:
- to - link/href to redirect, if no route found (
/*
regex matching) - fallback - boolean (
true
/false
).
If no available route found the firstfallback={true}
route will be used.
Not redirecting anywhere.
<a>
tag with e.preventDefault()
, to use this routing.
Props:
- href - link/href to redirect
- hrefMemo - link/href memo to redirect, if specified overwrites href prop (for dynamic)
- beforeRedirect - func that will be called onClick and before redirect
({href, e}) => {}
- afterRedirect - func that will be called onClick and after redirect
({href, e}) => {}
- children - default hidden prop, your elements
- ...otherProps - will be inserted in
<a>
declaration.
Ex:
import {Link} from "@gh0st-work/solid-js-router";
const PersonalAccount = () => {
return (
<>
<Link
href={'/home'}
class={'font-medium text-amber-500 hover:text-amber-400'}
beforeRedirect={({href, e}) => console.log(href, e)}
afterRedirect={({href, e}) => console.log(href, e)}
>
Go home
</Link>
<Link
href={'/home'}
class={'text-white font-medium text-lg bg-amber-500 hover:bg-amber-400 flex items-center justify-center space-x-2 rounded-md px-4 py-2'}
>
<i class={'w-4 h-4 fa-solid fa-house'}/>
<span>Go home button</span>
</Link>
</>
)
}
History navigation, all apis from history package.
And history.pathname()
(signal) used in routing.
Ex:
import {useHistory} from "@gh0st-work/solid-js-router";
const Home = () => {
const history = useHistory()
return (
<>
<span>Current pathname: {history.pathname()}</span>
<button onClick={() => history.push('/home')}>Go home</button>
<button onClick={() => history.back()}>Go back</button>
</>
)
}
- second-level nesting
- match params forwarding
- more levels nesting
- match params nesting forwarding
- necessary-only mount
- depsMemo
- TypeScript
- match params nesting forwarding clean & logic improve