_/use is a small derived module holding the hooks a page needs that its router doesn't already provide.
It does not exist for every framework
_/use is derived only for Vue, Svelte and MDX folders. React and SolidJS folders have no _/use at all - React Router and Solid Router already cover this ground, so you import from them instead. An import { ... } from "_/use" in a React or SolidJS folder will not resolve.
What each framework provides
| React | SolidJS | Vue | Svelte | MDX | |
|---|---|---|---|---|---|
_/use exists | ❌ | ❌ | ✅ | ✅ | ✅ |
useLoaderData | – | – | ✅ | ✅ | ✅ |
useRoute | – | – | – | ✅ | ✅ |
useParams | – | – | – | ✅ | ✅ |
useParamsEntries | – | – | – | ✅ | ✅ |
useSearchParams | – | – | – | ✅ | ✅ |
useFrontmatter | – | – | – | – | ✅ |
Where a cell is empty, use the framework's own equivalent:
| Need | React | SolidJS | Vue |
|---|---|---|---|
| loader data | useLoaderData from react-router | createAsync from @solidjs/router | useLoaderData from _/use |
| route params | useParams from react-router | useParams from @solidjs/router | useRoute from vue-router |
| search params | useSearchParams from react-router | useSearchParams from @solidjs/router | useRoute().query |
useLoaderData
useLoaderData<T>(key?: string): T | undefinedReads the result of the page's loader export, resolved before render.
<!-- Vue: pages/users/[id]/index.vue -->
<script setup lang="ts">
import { useLoaderData } from "_/use";
import type { ResponseT } from "_/fetch";
const user = useLoaderData<ResponseT["users/[id]"]["GET"]>();
</script>useLoaderData exists only where _/use does - Vue, Svelte and MDX. React reads loader data through useLoaderData from react-router, and SolidJS through createAsync from @solidjs/router.
Two things to keep in mind:
- It returns
T | undefined. The loader resolves before render in the normal case, but the type is honest about routes that declare no loader. Annotate the read and handle the empty case (v-if="user",{#if user}) rather than asserting. - In a layout, pass the key. Vue, Svelte and MDX share one per-route store keyed by route name, and the hook cannot tell which layout it is running in. A page passes nothing; a layout passes its path-qualified name:
const data = useLoaderData("dashboard/layout"); // pages/dashboard/layout.*useParams
useParams<RouteName>(): ParamsMap[RouteName]Svelte and MDX only.
On Svelte, pass the route name as a type argument and the params come back typed for that route - required params as values, optional ones possibly undefined, a splat as an array or undefined.
An MDX page calls useParams() with no type argument and gets the same values untyped:
<script lang="ts">
import { useParams } from "_/use";
const { slug } = useParams<"blog/[slug]">();
</script>useRoute
Returns the whole route context in one object - name, params, paramsEntries, searchParams, loaderData, and on MDX also frontmatter. Useful in a shared component (a breadcrumb, a title bar) that needs to know where it is without being handed props:
import { useRoute } from "_/use";
export default function Breadcrumb() {
const { name, params } = useRoute();
// ...
}useParamsEntries
Returns [keys, values] - both arrays in the order the route declares its parameters, which is exactly the order a fetch client expects:
const [keys, values] = useParamsEntries<"users/[id]/posts/[postId]">();
await fetchClients["users/[id]/posts/[postId]"].GET(values);In MDX, call useParamsEntries() without the type argument - the returned order is still correct, it is just untyped.
Prefer this over rebuilding an array from useParams(). Object key order is insertion order in practice, but it is not tied to how the route declares its parameters - useParamsEntries is correct by construction. Details ›
useSearchParams
Returns the current query string as parsed search params, for reading ?page=2&sort=date on the page side.
This is route-level search state. Validating and typing query parameters is a separate concern handled on the API contract, through the
queryvalidation target and the fetch clients. Details ›
useFrontmatter
MDX only. Reads the current page's YAML frontmatter, for dynamic head content or conditional rendering in a layout:
import { useFrontmatter } from "_/use";
export const Header = () => {
const { title } = useFrontmatter();
return title ? <h1>{title}</h1> : null;
};Two Rules That Bite
Hooks run during render, never at module scope. A hook call in a module-level export runs on import, before any component exists:
export const params = useParams(); // ❌ runs on import, fails
export const P = () => useParams(); // ✅ runs during renderThis is also why a loader cannot use hooks - it runs before the component tree exists. It receives the resolved route object as its argument instead. Details ›
On Svelte, read them during component initialisation. The Svelte hooks read Svelte context, so - like any getContext() call - they are only valid at the top level of a <script> block. Calling one inside an event handler or a callback that runs later will not work:
<script lang="ts">
import { useParams } from "_/use";
const { id } = useParams<"users/[id]">(); // ✅ top level
function onClick() {
const p = useParams<"users/[id]">(); // ❌ too late, no context
}
</script>