remix-utils: the small React Router helpers you would otherwise write yourself
A set of utility functions and types to use with Remix.run
At a glance
- What is it?
- A MIT licensed collection of utilities for React Router apps, shipped both as an npm package and as a shadcn registry, with CSRF middleware, hydration helpers and promise wrappers that each pull in their own optional dependency.
- Who is it for?
- remix-utils earns its 2,366 stars by being unopinionated about everything except the shape of the solution: each export is a single file with one job, the export map is explicit per path, and the optional dependency is named in the documentation of the utility that needs it. Read that dependency list before you install anything, because it is the real shape of the library.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 67 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 7, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A library that renamed itself in its own README
The repository description still reads "A set of utility functions and types to use with Remix.run", and the `package.json` keyword list still includes both `remix` and `remix.run`. The README has moved on: its first line describes a package containing simple utility functions to use with React Router. That gap is not sloppiness so much as a rename in progress. Remix merged into React Router, the package followed, and the version number is where the history shows up most clearly.
Version 10 is for React Router v8, and React Router v8 requires React 19.2.7 or newer and Node 22.22.0 or newer. The README states the fallback plainly: if your app is still on React Router v7, stay on Remix Utils 9.x. That is a hard floor, not a recommendation, so check your app's React Router major before upgrading the utilities or you will spend an afternoon on resolution errors that say nothing about the utility you were trying to add.
The rest of the shape is easy to read off the package manifest. It is TypeScript, MIT licensed, published as ESM with `sideEffects` set to false, and the shipped files are `build`, `package.json` and `README.md`. The author field names Sergio Xalambrí directly, and funding points at a single GitHub Sponsors profile. With 2,366 stars, 146 forks and 32 open issues, and a last push on 2026-08-01, this is an actively maintained package with a real user base rather than a project that got popular and stopped.
Two installation paths, and the second one is unusual
The ordinary path is a single command:
npm install remix-utilsThe unusual path is the one worth reading the README for. remix-utils publishes itself as a shadcn registry, which means you add a registry entry to your `components.json` and then pull in one utility at a time:
{
"registries": {
"@remix-utils": "https://sergiodxa.github.io/remix-utils/r/{name}.json"
}
}After that, `bunx shadcn@latest add @remix-utils/<item>` copies a single utility into your source tree, and you own it from then on. The release history explains how recent this is: v9.3.0, published 2026-03-11, added the full shadcn registry generated from the package exports and wired the registry output into the docs pages. v9.3.1 the same day documented the install commands, and expanded the per-module JSDoc from the README content.
There is a real trade-off here that the README does not argue about. The npm route keeps the library updatable through your lockfile and gives you a single dependency to audit. The registry route gives you source you can read and edit, with no dependency at all for a utility that is thirty lines of code, at the cost of owning the maintenance of that copy. For a security-adjacent utility like CSRF middleware, having the signing code in your repository is defensible. For `ClientOnly`, copying it in is hard to justify when the whole point was to avoid writing it.
The optional dependency list is the API's real shape
remix-utils does not bundle its dependencies. The README names nine of them as optional, and pairs them with utilities: `react-router`, `@edgefirst-dev/batcher`, `@edgefirst-dev/jwt`, `@edgefirst-dev/server-timing`, `@oslojs/crypto`, `@oslojs/encoding`, `is-ip`, `intl-parse-accept-language` and `react`. The rule the README gives is simple, and it is the most useful sentence in the whole file: install optional dependencies only when needed, and the utilities that require one say so in their documentation.
If you want everything in one go, the README gives the command:
npm add @edgefirst-dev/batcher @edgefirst-dev/jwt @edgefirst-dev/server-timing @oslojs/crypto @oslojs/encoding is-ip intl-parse-accept-languageNote what is missing from that list: `react` and `react-router`, because the README says those should already be installed in your project. That is a sensible default for a library targeting React Router apps, and it is also why the two React components below declare `react` as a dependency of their own.
This design explains the export map in `package.json`. Every utility gets its own subpath export with explicit types and a default build target, so `./middleware/cors`, `./middleware/csrf`, `./middleware/honeypot`, `./middleware/context-storage`, `./middleware/basic-auth`, `./middleware/server-timing` and the rest each resolve to a single compiled file. Nothing is bundled that you did not ask for, which is the correct default for a package whose whole job is to be assembled piece by piece.
promiseHash and timeout, the two promise wrappers
`promiseHash` is the one utility that has nothing to do with rendering. It is the object-shaped version of `Promise.all`: you pass an object of promises, you get back an object with the same keys holding the resolved values. The point is that the keys stay attached to the results, so a loader with three independent reads returns three named values instead of an array you have to destructure by position.
import { promiseHash } from "remix-utils/promise";
export async function loader({ request }: Route.LoaderArgs) {
return json(
await promiseHash({
user: getUser(request),
posts: getPosts(request),
}),
);
}The README notes that it nests, which matters more than it first appears. A page that needs a user, a list of posts, and per-post comments and likes can express that shape directly instead of awaiting in stages and reassembling by hand. The alternative in a typical loader is either `await` in sequence, which serializes independent reads, or a hand-rolled reducer that quietly loses a key when someone adds a field.
`timeout` wraps any promise with a deadline and rejects with a `TimeoutError` when the deadline passes. It also accepts an `AbortController`, and when you pass one, the timeout aborts the underlying operation rather than leaving it running:
import { timeout } from "remix-utils/promise";
try {
let controller = new AbortController();
let result = await timeout(fetch("https://example.com", { signal: controller.signal }), {
ms: 100,
controller,
});
} catch (error) {
if (error instanceof TimeoutError) {
// Handle timeout
}
}That controller parameter is the difference between a timeout that frees the request and one that leaks an in-flight fetch per call. For a loader proxying a third-party API, it is the detail that keeps a slow upstream from accumulating connections in your server process.
cacheAssets, and the one client-entry hook in the package
`cacheAssets` writes every JavaScript file the framework built into the browser's Cache Storage, so the next visit does not refetch them. It is a small function with two options, and both have defaults worth knowing: `cacheName` defaults to `assets`, and `buildPath` defaults to `/build/`, which matches the framework's own default build path.
import { cacheAssets } from "remix-utils/cache-assets";
cacheAssets().catch((error) => {
// do something with the error, or not
});The README is explicit about two constraints. It only runs inside `entry.client`, so calling it from a component does nothing useful. And if you changed the build path in your config, you must pass the same value to `cacheAssets`, or it will not find your files. A second option exists for the case where you added a service worker and want both to share one cache.
The README's own second example contains a typo, `cacheAssests` instead of `cacheAssets`, which is a small reminder that the registry and JSDoc were generated from the README text rather than written independently. It is harmless in prose and would be a runtime error if copied. The function itself is the kind of thing most apps end up writing by hand, and the fact that it lives in this package rather than in a React Router release says something about which framework features were considered core enough to leave to users.
ClientOnly and ServerOnly, and why the fallback is not optional
These two components decide which side of the render boundary a subtree appears on. `ClientOnly` renders its children only in the browser, which is how you mount a chart or a map component that has no server-side story. `ServerOnly` is the mirror image, for content that should never reach the client bundle.
import { ClientOnly } from "remix-utils/client-only";
export default function Component() {
return (
<ClientOnly fallback={<SimplerStaticVersion />}>
{() => <ComplexComponentNeedingBrowserEnvironment />}
</ClientOnly>
);
}The children are a function rather than an element, and that is the important detail. Passing a render function means the real component is not constructed during server rendering at all, so a browser-only library is never imported into that pass. The fallback prop is what gets rendered instead, and the README calls it optional while recommending it strongly, for a reason worth repeating: without a fallback you get layout shift when hydration swaps the real component in.
The rendering sequence the README documents is four steps. Server-side rendering always produces the fallback. The first client render also produces the fallback, because on that pass the component still does not know whether hydration has completed. An update after hydration renders the real thing. Every later render skips the fallback entirely. So you pay the fallback once on the server and once on the first client pass, and never again. `ClientOnly` gets that behaviour from a `useHydrated` hook internally, which is the piece you would otherwise have to write and get subtly wrong.
What v10.0.0 actually changed
The June 2026 release of v10.0.0 is where the security-relevant work landed, and it is worth reading the notes for what they reveal about the library's centre of gravity. Two changes touch CSRF: the token signing moved to HMAC-SHA256 with a constant-time comparison, and the middleware now uses the configured secret when signing and verifying tokens rather than falling back to something else.
The constant-time comparison is the fix that matters and the one most hand-rolled CSRF implementations get wrong. A byte-by-byte comparison returns as soon as two bytes differ, which leaks timing information about how much of an attacker's guess was correct. Anyone who wrote their own CSRF module should read that pull request before assuming their version is fine.
The rest of the release is spread across features: `ExternalScripts` gained support for global attributes including `data-*`, a `nonce` prop, and multi-line data handling while composing server-sent events. CSS files can now appear in the route manifest. `HoneypotInput` can be hidden by an external CSS class. A documentation fix added a missing context argument to `serverTimingMiddleware`.
That list also shows this is a community project, not a one-person package. The v10 notes credit contributors by handle for the HMAC work, the nonce prop, the route manifest change, the honeypot styling and the SSE fix. Twenty-odd release entries across three versions, with named external contributors and a code of conduct file in the tree, is what a healthy small project looks like from the outside.
The tooling tells you how the repo is maintained
A few files in the tree explain more about the project's habits than the README does. There is `typedoc.json` and a `docs/` directory, so the per-module API reference is generated from the source and published at the project homepage rather than hand-maintained. There is a `registry.json` at the root, which is the shadcn registry manifest that the `components.json` entry above points at. And there are both `bun.lock` and `package-lock.json`, plus `bunfig.toml`, which means the maintainer develops against Bun while keeping an npm lockfile for consumers.
The linting setup went through its own migration inside the release window. v9.3.0 records a move from Biome to Oxlint and Oxfmt, followed immediately by a repository-wide lint and format pass to absorb the switch. The `.oxlintrc.json` and `.oxfmtrc.json` files at the root are the residue. Two more fixes in the same release are the sort that only show up when you depend on other people's libraries: middleware test helper types updated for the latest React Router, and a JWK auth test fixed for a memory API change in `@remix-run/file-storage`.
The upgrade path is documented too, at `./docs/v6-to-v7.md`, referenced from the README. There is a `scripts/` directory and a `.github/` directory holding the release configuration that generates those changelogs, which is how the v10 notes arrived in that consistent grouped format. What you will not find is a changelog file in the root or a contributing guide beyond the code of conduct, so the repository is closer to a well-organised personal project than to a committee-governed one.
Editorial conclusion
remix-utils earns its 2,366 stars by being unopinionated about everything except the shape of the solution: each export is a single file with one job, the export map is explicit per path, and the optional dependency is named in the documentation of the utility that needs it. Read that dependency list before you install anything, because it is the real shape of the library. The shadcn route is the interesting one for a React Router v8 codebase, since a utility that only needs `@oslojs/encoding` should not drag your whole dependency graph in with it. Where the README runs out, the generated docs at the project homepage carry the per-module detail, and the `src/` directory is small enough that reading it is faster than guessing.
Frequently asked questions
What React Router version does remix-utils 10 require?
Remix Utils 10.x requires React Router v8, which in turn requires React 19.2.7 or newer and Node 22.22.0 or newer. If your application is still on React Router v7, the README says to stay on Remix Utils 9.x.
How do I install a single remix-utils utility instead of the whole package?
Add a registries entry pointing at https://sergiodxa.github.io/remix-utils/r/{name}.json to your components.json, then run bunx shadcn@latest add @remix-utils/<item>. The full shadcn registry was generated from the package exports in v9.3.0.
Do I need to install react-router separately to use remix-utils?
Yes. react-router and react are listed as optional dependencies that should already be present in your project, so the command for installing all of the other optional dependencies deliberately leaves them out.
What changed in remix-utils v10.0.0?
CSRF token signing moved to HMAC-SHA256 with a constant-time comparison, and the middleware now uses the configured secret when signing and verifying tokens. The release also added a nonce prop and data-* support to ExternalScripts, CSS files in the route manifest, and multi-line SSE data handling.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sergiodxa-remix-utils)