React Integration
Problem
Using useState + useEffect to bind external router state to React causes tearing: the render that reads state and the effect that subscribes to updates can be out of sync in concurrent mode.
Solution
Use useSyncExternalStore(subscribe, getSnapshot) for tear-safe, concurrent-mode-compatible route state. Create the router and bind actions at module scope outside the hook.
ts
// router.ts
import { createRouter } from '@vielzeug/wayfinder';
import { useSyncExternalStore } from 'react';
const router = createRouter({
routes: {
home: { component: HomePage, path: '/' },
settings: { component: SettingsPage, path: '/settings' },
notFound: { component: NotFoundPage, path: '*' },
},
});
// Stable bound references, safe to return from the hook.
const getSnapshot = () => router.getSnapshot();
const subscribe = (cb: () => void) => router.subscribe(cb);
const isActive = router.isActive.bind(router);
const navigate = router.navigate.bind(router);
const url = router.url.bind(router);
export function useRouter() {
const state = useSyncExternalStore(subscribe, getSnapshot);
return { isActive, navigate, state, url };
}tsx
// RouterView.tsx
import { useRouter } from './router';
type RouteComponent = React.ComponentType | undefined;
export function RouterView() {
const { state } = useRouter();
const Component = state.matches.at(-1)?.component as RouteComponent;
return Component ? <Component /> : null;
}tsx
// RouterLink.tsx
import { useRouter } from './router';
type LinkName = 'home' | 'settings' | 'notFound';
type Props = {
children: React.ReactNode;
name: LinkName;
};
export function RouterLink({ children, name }: Props) {
const { isActive, navigate, url } = useRouter();
const href = url(name);
const active = isActive(name);
return (
<a
aria-current={active ? 'page' : undefined}
href={href}
onClick={(event) => {
event.preventDefault();
void navigate({ name });
}}>
{children}
</a>
);
}Pitfalls
- Never use
useEffect+useStatefor router state in concurrent mode — it causes tearing between reads and subscription updates. - Do not create
getSnapshot,subscribe,navigate,url, orisActiveinside the hook. They must be stable references at module scope to avoid infinite re-renders.