Not supported by design
Dusk 1.0.0 does not implement two Next.js routing features, on purpose.
Dusk 1.0.0 does not implement two Next.js routing features, on purpose. Both are out of scope for 1.0.0; neither is on the roadmap for this release.
- Parallel routes (
@slotfolders). A layout that renders several routes at once, each with its own loading and error state, needs the router to match one URL to several routes and to keep each one's pending state apart. Dusk matches one route tree per URL, with one<Outlet />per layout, so a parallel route has no place in that model. - Intercepting routes (
(.)photo/[id]). A route that shows inside the current page when the link is followed, and as a full page on a reload, needs the same URL to render two different trees depending on how it was reached. Dusk renders the route the URL names, every time.
Build the same screens with what Dusk does have:
- Parallel regions become nested layouts. A dashboard with a sidebar and a feed is a
_layout.tsxthat renders the sidebar, and child routes that render the feed. Each route loads its own data in its ownloader, and a slow child shows its own pending state through aSuspenseboundary in the layout. - A modal is URL state, not a route. Keep the open photo in the search params (or in the hash when a change should not
rerun the loader), and render a
Dialogfrom@shadow-js/primitiveswithopenandonOpenChange. The URL can be shared, a reload shows the same dialog on the same page, and the back button closes it.
src/routes/gallery.tsxtsx
import { Dialog, DialogContent, DialogTitle } from "@shadow-js/primitives/dialog";
import { type LoaderArgs, useLoaderData, useNavigate, useSearch } from "@shadow-js/router";
import { HttpError } from "@shadow-js/dusk";
interface Search {
photo?: string;
}
export const validateSearch = (search: Record<string, unknown>): Search => {
const photo = search["photo"];
if (photo === undefined) return {};
if (typeof photo !== "string" || !/^[a-z0-9-]{1,40}$/.test(photo)) {
throw new HttpError(400, "photo must be a photo id");
}
return { photo };
};
// The grid is one loader; the open photo is loaded by the same loader when the search names it.
export const loader = async ({ search }: LoaderArgs<"/gallery", Search>) => ({
photos: await listPhotos(),
open: search.photo === undefined ? undefined : await findPhoto(search.photo),
});
export default function Gallery() {
const data = useLoaderData<typeof loader>();
const search = useSearch<Search>();
const navigate = useNavigate();
return (
<>
<ul>
{data().photos.map((photo) => (
<li>
<a href={`?photo=${photo.id}`}>{photo.title}</a>
</li>
))}
</ul>
<Dialog
open={search.photo !== undefined}
onOpenChange={(open) => {
if (!open) navigate("/gallery", { search: { photo: undefined }, replace: true });
}}
>
<DialogContent>
<DialogTitle>{data().open?.title ?? "Photo"}</DialogTitle>
</DialogContent>
</Dialog>
</>
);
}
listPhotos and findPhoto stand for your data access; keep them in server code (a server function or a *.server.ts
module) so that only what the page returns reaches the browser. The loader runs on the server for a reload and in the browser
for a link, so the open photo is the same either way.