[]previous and next page

Solid Router

@shadow-js/router is built around a router instance and route objects, not around <Router> and <Route> elements.

For agentsUpdated

@shadow-js/router is built around a router instance and route objects, not around <Router> and <Route> elements. Layouts are routes with a component and an <Outlet />; data comes from a route loader. The core (matching, history, navigation state machine) is in @shadow-js/router/core.

Solid Router (not executed):

tsx
import { A, Route, Router, useNavigate, useParams, useSearchParams, createAsync, query } from "@solidjs/router";

const getPost = query(async (id) => (await fetch(`/api/posts/${id}`)).json(), "post");

function Post() {
  const params = useParams();
  const post = createAsync(() => getPost(params.id));
  const [search, setSearch] = useSearchParams();
  return <h1>{post()?.title}</h1>;
}

<Router root={Layout}>
  <Route path="/" component={Home} />
  <Route path="/posts/:id" component={Post} preload={({ params }) => getPost(params.id)} />
  <Route path="*404" component={NotFound} />
</Router>;

ShadowJS:

src/app-routes.tsxtsx
import {
  A,
  createMemoryHistory,
  createRouter,
  defineRoute,
  notFound,
  Outlet,
  redirect,
  useLoaderData,
  useLocation,
  useNavigate,
  useNavigation,
  useParams,
  useSearch,
} from "@shadow-js/router";
import { loaderCache } from "@shadow-js/router/loaders";
import { notFoundUi } from "@shadow-js/router/not-found";
import { searchValidation } from "@shadow-js/router/search";
import { Show } from "@shadow-js/web";

export interface Post {
  id: string;
  title: string;
}

const posts: Record<string, Post> = {
  "1": { id: "1", title: "First" },
  "2": { id: "2", title: "Second" },
};

/** Ids the loader ran for, so the tests can count loads. */
export const loads: string[] = [];

async function loadPost(id: string): Promise<Post> {
  loads.push(id);
  const found = posts[id];
  if (found === undefined) throw notFound();
  return found;
}

function Layout() {
  const navigation = useNavigation();
  return (
    <div>
      <nav>
        <A to="/" end activeClass="active">
          Home
        </A>{" "}
        <A to="/posts/:id" params={{ id: "2" }}>
          Post 2
        </A>{" "}
        <A to="/posts" search={{ page: 1 }}>
          Posts
        </A>
      </nav>
      <Show when={navigation.pending}>
        <p>loading</p>
      </Show>
      <Outlet />
    </div>
  );
}

function Home() {
  return <h1>Home</h1>;
}

function PostPage() {
  const params = useParams({ from: "/posts/:id" });
  const post = useLoaderData<Post>({ from: "/posts/:id" });
  return (
    <h1>
      {post()?.title} ({params.id})
    </h1>
  );
}

function PostList() {
  const search = useSearch<{ page: number }>({ from: "/posts" });
  const location = useLocation();
  const go = useNavigate();
  return (
    <div>
      <p>page {search.page}</p>
      <button type="button" onClick={() => go(location.pathname, { search: { page: search.page + 1 } })}>
        Next page
      </button>
    </div>
  );
}

const home = defineRoute("/", { component: Home });
const post = defineRoute("/posts/:id", {
  loader: ({ params }) => loadPost(params.id),
  component: PostPage,
  notFound: () => <p>No such post</p>,
});
const list = defineRoute("/posts", {
  validateSearch: (search) => ({ page: Number(search["page"] ?? 1) }),
  component: PostList,
});
const legacy = defineRoute("/old", {
  beforeLoad: () => {
    throw redirect("/");
  },
});
const missing = defineRoute("/*", { component: () => <p>Page not found</p> });
const root = defineRoute("", { id: "root", component: Layout, children: [home, post, list, legacy, missing] });

export function createApp(url: string) {
  return createRouter({
    routes: [root],
    history: createMemoryHistory({ initialEntries: [url] }),
    features: [loaderCache(), notFoundUi(), searchValidation()],
  });
}
Solid Router ShadowJS What differs
<Router root={Layout}> createRouter({ routes, history?, base?, ... }) and <RouterProvider router={router}><Outlet /></RouterProvider> The router is a value you create. routes are defineRoute objects; a layout is a route (usually pathless: defineRoute("", { id, component, children })). Without a history option the browser history of window is used, or memory history when there is no window.
<HashRouter>, <MemoryRouter>, <StaticRouter url> createRouter({ history: createHashHistory() }), createMemoryHistory({ initialEntries }) createBrowserHistory is also exported. For SSR use memory history and await router.ready() before rendering.
<Route path component preload /> defineRoute(path, { component, loader, beforeLoad, children, ... }) There is no <Route> element and no array of paths per route. props.children of a layout becomes <Outlet />.
/users/:id, /users/:id?, *splat, *404 /users/:id, /users/:id?, /files/*rest or * A splat is the last segment and captures the rest of the path under its name (* when bare). Match filters (matchFilters) do not exist.
<A href end activeClass inactiveClass> <A to end activeClass params search hash replace state preload> to (or href) takes a route pattern with params or a path. activeClass is added while active and aria-current="page" is set on an exact match. No inactiveClass, no noScroll. preload is "intent" (default), "viewport" or false.
<Navigate href /> <Navigate to params search hash replace /> Navigates when rendered, in the browser only. For route-dependent redirects throw redirect() from beforeLoad or loader.
useParams() useParams({ from? }) A read-only view with one memo per key: a reader of id does not re-run when slug changes.
useSearchParams() useSearchParams(validate?) from @shadow-js/router (returns [read, set]) validate types the query. set({ key: value }) merges the keys into the current query, undefined removes a key, and setters in one tick become one navigation. set(prev => ...) takes a function. { replace: true } replaces the entry.
useNavigate() useNavigate() (to, { replace, state, params, search, hash }) returns a promise of the outcome (success, notFound, ...); it never throws for a redirect or a canceled navigation. No scroll or resolve option.
useLocation() useLocation() { pathname, search, hash }, reactive per property. No query, state or key.
useMatch(() => path) useMatch({ from }) Returns an accessor of the rendered match for a route id (the route's path by default), or undefined. It does not test an arbitrary pattern.
useCurrentMatches() useMatches() Accessor of the rendered chain, outermost first.
useIsRouting() useNavigation() { pending, status, location }. pending becomes true after pendingMs (default 1000 ms) and stays true for at least pendingMinMs (500 ms). Route-level pending components exist too.
useBeforeLeave(cb) useBeforeLeave(listener), useBlocker(shouldBlock) from @shadow-js/router/blockers (with blockers() in features) event.preventDefault() keeps the user on the page; useBlocker returns status(), proceed() and reset(). In-app transitions only, not tab close.
preload, query, createAsync loader({ params, search, context, location, intent, signal }) and useLoaderData() useLoaderData returns an accessor. Loaders of all matched routes run in parallel after beforeLoad chain. The result is cached per route and loaderDeps. staleReload chooses blocking or background refresh.
revalidate(key) router.invalidate(filter?) Marks loader data stale (all, or by id) and reloads the current location.
<A preload> / usePreloadRoute <A preload> / router.preload(to, options) Fills the cache without navigating.
redirect() redirect(to) Thrown from beforeLoad or loader.
action, useSubmission, <form action> none Use onSubmit, an async function, useOptimistic, router.invalidate().
error and 404 handling route options error, notFound, pending, head error({ error, reset }) replaces the route; notFound() thrown from a loader renders the nearest route's notFound.
base base option Matched on a segment boundary.
scroll restoration scrollRestoration() feature (@shadow-js/router/scroll) Off unless it is in createRouter({ features }); it only acts when the router has a window.
SSR (url, dehydrate) createMemoryHistory({ initialEntries: [url] }), router.ready(), router.dehydrate(), router.hydrate(data), serializeHydrationData, parseHydrationData Loader data travels in a JSON-serializable object.