This package contains simple utility functions to use with [React Router](https://reactrouter.com/).
npm install remix-utilsThis package contains simple utility functions to use with React Router.
``bash`
npm install remix-utils
Additional optional dependencies may be needed, all optional dependencies are:
- react-router@edgefirst-dev/batcher
- @edgefirst-dev/jwt
- @edgefirst-dev/server-timing
- @oslojs/crypto
- @oslojs/encoding
- is-ip
- intl-parse-accept-language
- react
-
The utils that require an extra optional dependency mention it in their documentation.
If you want to install them all run:
`sh`
npm add @edgefirst-dev/batcher @edgefirst-dev/jwt @edgefirst-dev/server-timing @oslojs/crypto @oslojs/encoding is-ip intl-parse-accept-language
React and React Router packages should be already installed in your project.
Check the v6 to v7 upgrade guide.
The promiseHash function is not directly related to Remix but it's a useful function when working with loaders and actions.
This function is an object version of Promise.all which lets you pass an object with promises and get an object with the same keys with the resolved values.
`ts
import { promiseHash } from "remix-utils/promise";
export async function loader({ request }: Route.LoaderArgs) {
return json(
await promiseHash({
user: getUser(request),
posts: getPosts(request),
}),
);
}
`
You can use nested promiseHash to get a nested object with resolved values.
`ts
import { promiseHash } from "remix-utils/promise";
export async function loader({ request }: Route.LoaderArgs) {
return json(
await promiseHash({
user: getUser(request),
posts: promiseHash({
list: getPosts(request),
comments: promiseHash({
list: getComments(request),
likes: getLikes(request),
}),
}),
}),
);
}
`
The timeout function lets you attach a timeout to any promise, if the promise doesn't resolve or reject before the timeout, it will reject with a TimeoutError.
`ts
import { timeout } from "remix-utils/promise";
try {
let result = await timeout(fetch("https://example.com"), { ms: 100 });
} catch (error) {
if (error instanceof TimeoutError) {
// Handle timeout
}
}
`
Here the fetch needs to happen in less than 100ms, otherwise it will throw a TimeoutError.
If the promise is cancellable with an AbortSignal you can pass the AbortController to the timeout function.
`ts
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
}
}
`
Here after 100ms, timeout will call controller.abort() which will mark the controller.signal as aborted.
> [!NOTE]
> This can only be run inside entry.client.
This function lets you easily cache inside the browser's Cache Storage every JS file built by Remix.
To use it, open your entry.client file and add this:
`ts
import { cacheAssets } from "remix-utils/cache-assets";
cacheAssets().catch((error) => {
// do something with the error, or not
});
`
The function receives an optional options object with two options:
- cacheName is the name of the Cache object to use, the default value is assets.buildPath
- is the pathname prefix for all Remix built assets, the default value is /build/ which is the default build path of Remix itself.
It's important that if you changed your build path in remix.config.js you pass the same value to cacheAssets or it will not find your JS files.
The cacheName can be left as is unless you're adding a Service Worker to your app and want to share the cache.
`ts
import { cacheAssets } from "remix-utils/cache-assets";
cacheAssests({ cacheName: "assets", buildPath: "/build/" }).catch((error) => {
// do something with the error, or not
});
`
> [!NOTE]
> This depends on react.
The ClientOnly component lets you render the children element only on the client-side, avoiding rendering it the server-side.
You can provide a fallback component to be used on SSR, and while optional, it's highly recommended to provide one to avoid content layout shift issues.
`tsx
import { ClientOnly } from "remix-utils/client-only";
export default function Component() {
return (
{() =>
);
}
`
This component is handy when you have some complex component that needs a browser environment to work, like a chart or a map. This way, you can avoid rendering it server-side and instead use a simpler static version like an SVG or even a loading UI.
The rendering flow will be:
- SSR: Always render the fallback.
- CSR First Render: Always render the fallback.
- CSR Update: Update to render the actual component.
- CSR Future Renders: Always render the actual component, don't bother to render the fallback.
This component uses the useHydrated hook internally.
> [!NOTE]
> This depends on react.
The ServerOnly component is the opposite of the ClientOnly component, it lets you render the children element only on the server-side, avoiding rendering it the client-side.
You can provide a fallback component to be used on CSR, and while optional, it's highly recommended to provide one to avoid content layout shift issues, unless you only render visually hidden elements.
`tsx
import { ServerOnly } from "remix-utils/server-only";
export default function Component() {
return (
{() =>
);
}
`
This component is handy to render some content only on the server-side, like a hidden input you can later use to know if JS has loaded.
Consider it like the
The rendering flow will be:
- SSR: Always render the children.
- CSR First Render: Always render the children.
- CSR Update: Update to render the fallback component (if defined).
- CSR Future Renders: Always render the fallback component, don't bother to render the children.
This component uses the useHydrated hook internally.
The CORS function let you implement CORS headers on your loaders and actions so you can use them as an API for other client-side applications.
There are two main ways to use the cors function.
1. Use it on each loader/action where you want to enable it.
2. Use it globally on entry.server handleRequest and handleDataRequest export.
If you want to use it on every loader/action, you can do it like this:
`ts
import { cors } from "remix-utils/cors";
export async function loader({ request }: Route.LoaderArgs) {
let data = await getData(request);
let response = json
return await cors(request, response);
}
`
You could also do the json and cors call in one line.
`ts
import { cors } from "remix-utils/cors";
export async function loader({ request }: Route.LoaderArgs) {
let data = await getData(request);
return await cors(request, json
}
`
And because cors mutates the response, you can also call it and later return.
`ts
import { cors } from "remix-utils/cors";
export async function loader({ request }: Route.LoaderArgs) {
let data = await getData(request);
let response = json
await cors(request, response); // this mutates the Response object
return response; // so you can return it here
}
`
If you want to setup it globally once, you can do it like this in entry.server
`tsx
import { cors } from "remix-utils/cors";
const ABORT_DELAY = 5000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
let callbackName = isbot(request.headers.get("user-agent"))
? "onAllReady"
: "onShellReady";
return new Promise((resolve, reject) => {
let didError = false;
let { pipe, abort } = renderToPipeableStream(
{
[callbackName]: () => {
let body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
cors(
request,
new Response(body, {
headers: responseHeaders,
status: didError ? 500 : responseStatusCode,
}),
).then((response) => {
resolve(response);
});
pipe(body);
},
onShellError: (err: unknown) => {
reject(err);
},
onError: (error: unknown) => {
didError = true;
console.error(error);
},
},
);
setTimeout(abort, ABORT_DELAY);
});
}
export let handleDataRequest: HandleDataRequestFunction = async (
response,
{ request },
) => {
return await cors(request, response);
};
`
#### Options
Additionally, the cors function accepts a options object as a third optional argument. These are the options.
- origin: Configures the Access-Control-Allow-Origin CORS header.true
Possible values are:
- : Enable CORS for any origin (same as "\*")false
- : Don't setup CORSstring
- : Set to a specific origin, if set to "\*" it will allow any originRegExp
- : Set to a RegExp to match against the originArray
- : Set to an array of origins to match against theFunction
string or RegExp
- : Set to a function that will be called with the request origintrue
and should return a boolean indicating if the origin is allowed or not.
The default value is .methods
- : Configures the Access-Control-Allow-Methods CORS header.["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"]
The default value is .allowedHeaders
- : Configures the Access-Control-Allow-Headers CORS header.exposedHeaders
- : Configures the Access-Control-Expose-Headers CORS header.credentials
- : Configures the Access-Control-Allow-Credentials CORS header.maxAge
- : Configures the Access-Control-Max-Age CORS header.
> [!NOTE]
> This depends on react, @oslojs/crypto, @oslojs/encoding, and React Router.
The CSRF related functions let you implement CSRF protection on your application.
This part of Remix Utils needs React and server-side code.
First create a new CSRF instance.
`ts
// app/utils/csrf.server.ts
import { CSRF } from "remix-utils/csrf/server";
import { createCookie } from "react-router"; // or cloudflare/deno
export const cookie = createCookie("csrf", {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
secrets: ["s3cr3t"],
});
export const csrf = new CSRF({
cookie,
// what key in FormData objects will be used for the token, defaults to csrf`
formDataKey: "csrf",
// an optional secret used to sign the token, recommended for extra safety
secret: "s3cr3t",
});
Then you can use csrf to generate a new token.
`ts
import { csrf } from "~/utils/csrf.server";
export async function loader({ request }: Route.LoaderArgs) {
let token = csrf.generate();
}
`
You can customize the token size by passing the byte size, the default one is 32 bytes which will give you a string with a length of 43 after encoding.
`ts`
let token = csrf.generate(64); // customize token length
You will need to save this token in a cookie and also return it from the loader. For convenience, you can use the CSRF#commitToken helper.
`ts
import { csrf } from "~/utils/csrf.server";
export async function loader({ request }: Route.LoaderArgs) {
let [token, cookieHeader] = await csrf.commitToken(request);
return json({ token }, { headers: { "set-cookie": cookieHeader } });
}
`
> [!NOTE]
> You could do this on any route, but I recommend you to do it on the root loader.
Now that you returned the token and set it in a cookie, you can use the AuthenticityTokenProvider component to provide the token to your React components.
`tsx
import { AuthenticityTokenProvider } from "remix-utils/csrf/react";
let { csrf } = useLoaderData
return (
);
`
Render it in your root component and wrap the Outlet with it.
When you create a form in some route, you can use the AuthenticityTokenInput component to add the authenticity token to the form.
`tsx
import { Form } from "react-router";
import { AuthenticityTokenInput } from "remix-utils/csrf/react";
export default function Component() {
return (
Note that the authenticity token is only really needed for a form that mutates the data somehow. If you have a search form making a GET request, you don't need to add the authenticity token there.
This
AuthenticityTokenInput will get the authenticity token from the AuthenticityTokenProvider component and add it to the form as the value of a hidden input with the name csrf. You can customize the field name using the name prop.`tsx
`You should only customize the name if you also changed it on
createAuthenticityToken.If you need to use
useFetcher (or useSubmit) instead of Form you can also get the authenticity token with the useAuthenticityToken hook.`tsx
import { useFetcher } from "react-router";
import { useAuthenticityToken } from "remix-utils/csrf/react";export function useMarkAsRead() {
let fetcher = useFetcher();
let csrf = useAuthenticityToken();
return function submit(data) {
fetcher.submit(
{ csrf, ...data },
{ action: "/api/mark-as-read", method: "post" },
);
};
}
`Finally, you need to validate the authenticity token in the action that received the request.
`ts
import { CSRFError } from "remix-utils/csrf/server";
import { redirectBack } from "remix-utils/redirect-back";
import { csrf } from "~/utils/csrf.server";export async function action({ request }: Route.ActionArgs) {
try {
await csrf.validate(request);
} catch (error) {
if (error instanceof CSRFError) {
// handle CSRF errors
}
// handle other possible errors
}
// here you know the request is valid
return redirectBack(request, { fallback: "/fallback" });
}
`If you need to parse the body as FormData yourself (e.g. to support file uploads) you can also call
CSRF#validate with the FormData and Headers objects.`ts
let formData = await parseMultiPartFormData(request);
try {
await csrf.validate(formData, request.headers);
} catch (error) {
// handle errors
}
`> [!WARNING]
> If you call
CSRF#validate with the request instance, but you already read its body, it will throw an error.In case the CSRF validation fails, it will throw a
CSRFError which can be used to correctly identify it against other possible errors that may get thrown.The list of possible error messages are:
-
missing_token_in_cookie: The request is missing the CSRF token in the cookie.
- invalid_token_in_cookie: The CSRF token is not valid (is not a string).
- tampered_token_in_cookie: The CSRF token doesn't match the signature.
- missing_token_in_body: The request is missing the CSRF token in the body (FormData).
- mismatched_token: The CSRF token in the cookie and the body don't match.You can use
error.code to check one of the error codes above, and error.message to get a human friendly description.> [!WARNING]
> Don't send those error messages to the end-user, they are meant to be used for debugging purposes only.
$3
`ts
import { ExistingSearchParams } from "remix-utils/existing-search-params";
`> [!NOTE]
> This depends on
react and react-routerWhen you submit a GET form, the browser will replace all of the search params in the URL with your form data. This component copies existing search params into hidden inputs so they will not be overwritten.
The
exclude prop accepts an array of search params to exclude from the hidden inputs- add params handled by this form to this list
- add params from other forms you want to clear on submit
For example, imagine a table of data with separate form components for pagination and filtering and searching. Changing the page number should not affect the search or filter params.
`tsx
`By excluding the
page param, from the search form, the user will return to the first page of search result.`tsx
`$3
> [!NOTE]
> This depends on
react, and react-router.If you need to load different external scripts on certain routes, you can use the
ExternalScripts component together with the ExternalScriptsFunction and ScriptDescriptor types.In the route you want to load the script add a
handle export with a scripts method, type the handle to be ExternalScriptsHandle. This interface is let's you define scripts as either a function or an array.If you want to define what scripts to load based on the loader data, you can use
scripts as a function:`ts
import { ExternalScriptsHandle } from "remix-utils/external-scripts";type LoaderData = SerializeFrom;
export let handle: ExternalScriptsHandle = {
scripts({ id, data, params, matches, location, parentsData }) {
return [
{
src: "https://unpkg.com/htmx.org@1.9.6",
integrity: "sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni",
crossOrigin: 'anonymous"
}
];
},
};
`If the list of scripts to load is static you can define
scripts as an array directly.`ts
import { ExternalScriptsHandle } from "remix-utils/external-scripts";export let handle: ExternalScriptsHandle = {
scripts: [
{
src: "https://unpkg.com/htmx.org@1.9.6",
integrity: "sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni",
crossOrigin: 'anonymous",
preload: true, // use it to render a for this script
}
],
};
`You can also import
ExternalScriptsFunction and ScriptDescriptor interfaces yourself to build a custom handle type.`ts
import {
ExternalScriptsFunction,
ScriptDescriptor,
} from "remix-utils/external-scripts";interface AppHandle {
scripts?: ExternalScriptsFunction | ScriptDescriptor[];
}
export let handle: AppHandle = {
scripts, // define scripts as a function or array here
};
`Or you can extend the
ExternalScriptsHandle interface.`ts
import { ExternalScriptsHandle } from "remix-utils/external-scripts";interface AppHandle
extends ExternalScriptsHandle {
// more handle properties here
}
export let handle: AppHandle = {
scripts, // define scripts as a function or array here
};
`---
Then, in the root route, add the
ExternalScripts component somewhere, usually you want to load it either inside or at the bottom of , either before or after the Remix's component.Where exactly to place
will depend on your app, but a safe place is at the end of .`tsx
import { Links, LiveReload, Meta, Scripts, ScrollRestoration } from "remix";
import { ExternalScripts } from "remix-utils/external-scripts";type Props = { children: React.ReactNode; title?: string };
export function Document({ children, title }: Props) {
return (
{title ? {title} : null}
{children}
);
}
`Now, any script you defined in the ScriptsFunction will be added to the HTML.
You could use this util together with
useShouldHydrate to disable Remix scripts in certain routes but still load scripts for analytics or small features that need JS but don't need the full app JS to be enabled.`tsx
let shouldHydrate = useShouldHydrate();return (
{title ? {title} : null}
{children}
{shouldHydrate ? : }
);
`$3
> [!NOTE]
> This depends on
react, and react-router.This hook allows you to read the value of
transition.state, every fetcher.state in the app, and revalidator.state.`ts
import { useGlobalNavigationState } from "remix-utils/use-global-navigation-state";export function GlobalPendingUI() {
let states = useGlobalNavigationState();
if (state.includes("loading")) {
// The app is loading.
}
if (state.includes("submitting")) {
// The app is submitting.
}
// The app is idle
}
`The return value of
useGlobalNavigationState can be "idle", "loading" or "submitting"> [!NOTE]
> This is used by the hooks below to determine if the app is loading, submitting or both (pending).
$3
> [!NOTE]
> This depends on
react, and react-router.This hook lets you know if the global navigation, if one of any active fetchers is either loading or submitting, or if the revalidator is running.
`ts
import { useGlobalPendingState } from "remix-utils/use-global-navigation-state";export function GlobalPendingUI() {
let globalState = useGlobalPendingState();
if (globalState === "idle") return null;
return ;
}
`The return value of
useGlobalPendingState is either "idle" or "pending".> [!NOTE]
> This hook combines the
useGlobalSubmittingState and useGlobalLoadingState hooks to determine if the app is pending.> [!NOTE]
> The
pending state is a combination of the loading and submitting states introduced by this hook.$3
> [!NOTE]
> This depends on
react, and react-router.This hook lets you know if the global transition or if one of any active fetchers is submitting.
`ts
import { useGlobalSubmittingState } from "remix-utils/use-global-navigation-state";export function GlobalPendingUI() {
let globalState = useGlobalSubmittingState();
if (globalState === "idle") return null;
return ;
}
`The return value of
useGlobalSubmittingState is either "idle" or "submitting".$3
> [!NOTE]
> This depends on
react, and react-router.This hook lets you know if the global transition, if one of any active fetchers is loading, or if the revalidator is running
`ts
import { useGlobalLoadingState } from "remix-utils/use-global-navigation-state";export function GlobalPendingUI() {
let globalState = useGlobalLoadingState();
if (globalState === "idle") return null;
return ;
}
`The return value of
useGlobalLoadingState is either "idle" or "loading".$3
> [!NOTE]
> This depends on
react.This hook lets you detect if your component is already hydrated. This means the JS for the element loaded client-side and React is running.
With useHydrated, you can render different things on the server and client while ensuring the hydration will not have a mismatched HTML.
`ts
import { useHydrated } from "remix-utils/use-hydrated";export function Component() {
let isHydrated = useHydrated();
if (isHydrated) {
return ;
}
return ;
}
`When doing SSR, the value of
isHydrated will always be false. The first client-side render isHydrated will still be false, and then it will change to true.After the first client-side render, future components rendered calling this hook will receive
true as the value of isHydrated. This way, your server fallback UI will never be rendered on a route transition.$3
> [!NOTE]
> This depends on
react.This hook lets you get the locales returned by the root loader. It follows a simple convention, your root loader return value should be an object with the key
locales.You can combine it with
getClientLocal to get the locales on the root loader and return that. The return value of useLocales is a Locales type which is string | string[] | undefined.`ts
import { useLocales } from "remix-utils/locales/react";
import { getClientLocales } from "remix-utils/locales/server";// in the root loader
export async function loader({ request }: Route.LoaderArgs) {
let locales = getClientLocales(request);
return json({ locales });
}
// in any route (including root!)
export default function Component() {
let locales = useLocales();
let date = new Date();
let dateTime = date.toISOString;
let formattedDate = date.toLocaleDateString(locales, options);
return ;
}
`The return type of
useLocales is ready to be used with the Intl API.$3
> [!NOTE]
> This depends on
react-router and react.If you are building a Remix application where most routes are static, and you want to avoid loading client-side JS, you can use this hook, plus some conventions, to detect if one or more active routes needs JS and only render the Scripts component in that case.
In your document component, you can call this hook to dynamically render the Scripts component if needed.
`tsx
import type { ReactNode } from "react";
import { Links, LiveReload, Meta, Scripts } from "react-router";
import { useShouldHydrate } from "remix-utils/use-should-hydrate";interface DocumentProps {
children: ReactNode;
title?: string;
}
export function Document({ children, title }: DocumentProps) {
let shouldHydrate = useShouldHydrate();
return (
{title ? {title} : null}
{children}
{shouldHydrate && }
);
}
`Now, you can export a
handle object with the hydrate property as true in any route module.`ts
export let handle = { hydrate: true };
`This will mark the route as requiring JS hydration.
In some cases, a route may need JS based on the data the loader returned. For example, if you have a component to purchase a product, but only authenticated users can see it, you don't need JS until the user is authenticated. In that case, you can make
hydrate be a function receiving your loader data.`ts
export let handle = {
hydrate(data: LoaderData) {
return data.user.isAuthenticated;
},
};
`The
useShouldHydrate hook will detect hydrate as a function and call it using the route data.$3
> [!NOTE]
> This depends on
is-ip.This function receives a Request or Headers objects and will try to get the IP address of the client (the user) who originated the request.
`ts
import { getClientIPAddress } from "remix-utils/get-client-ip-address";export async function loader({ request }: Route.LoaderArgs) {
// using the request
let ipAddress = getClientIPAddress(request);
// or using the headers
let ipAddress = getClientIPAddress(request.headers);
}
`If it can't find he IP address the return value will be
null. Remember to check if it was able to find it before using it.The function uses the following list of headers, in order of preference:
- X-Client-IP
- X-Forwarded-For
- HTTP-X-Forwarded-For
- Fly-Client-IP
- CF-Connecting-IP
- Fastly-Client-Ip
- True-Client-Ip
- X-Real-IP
- X-Cluster-Client-IP
- X-Forwarded
- Forwarded-For
- Forwarded
- DO-Connecting-IP
- oxygen-buyer-ip
When a header is found that contains a valid IP address, it will return without checking the other headers.
> [!WARNING]
> On local development the function is most likely to return
null. This is because the browser doesn't send any of the above headers, if you want to simulate those headers you will need to either add it to the request Remix receives in your HTTP server or run a reverse proxy like NGINX that can add them for you.$3
> [!NOTE]
> This depends on
intl-parse-accept-language.This function let you get the locales of the client (the user) who originated the request.
`ts
import { getClientLocales } from "remix-utils/locales/server";export async function loader({ request }: Route.LoaderArgs) {
// using the request
let locales = getClientLocales(request);
// or using the headers
let locales = getClientLocales(request.headers);
}
`The return value is a Locales type, which is
string | string[] | undefined.The returned locales can be directly used on the Intl API when formatting dates, numbers, etc.
`ts
import { getClientLocales } from "remix-utils/locales/server";
export async function loader({ request }: Route.LoaderArgs) {
let locales = getClientLocales(request);
let nowDate = new Date();
let formatter = new Intl.DateTimeFormat(locales, {
year: "numeric",
month: "long",
day: "numeric",
});
return json({ now: formatter.format(nowDate) });
}
`The value could also be returned by the loader and used on the UI to ensure the user's locales is used on both server and client formatted dates.
$3
This function let you identify if a request was created because of a prefetch triggered by using
or .This will let you implement a short cache only for prefetch requests so you avoid the double data request.
`ts
import { isPrefetch } from "remix-utils/is-prefetch";export async function loader({ request }: Route.LoaderArgs) {
let data = await getData(request);
let headers = new Headers();
if (isPrefetch(request)) {
headers.set("Cache-Control", "private, max-age=5, smax-age=0");
}
return json(data, { headers });
}
`$3
#### Redirect Back
This function is a wrapper of the
redirect helper from Remix. Unlike Remix's version, this one receives the whole request object as the first value and an object with the response init and a fallback URL.The response created with this function will have the
Location header pointing to the Referer header from the request, or if not available, the fallback URL provided in the second argument.`ts
import { redirectBack } from "remix-utils/redirect-back";export async function action({ request }: Route.ActionArgs) {
throw redirectBack(request, { fallback: "/" });
}
`This helper is most useful when used in a generic action to send the user to the same URL it was before.
#### Not Modified
Helper function to create a Not Modified (304) response without a body and any header.
`ts
import { notModified } from "remix-utils/responses";export async function loader({ request }: Route.LoaderArgs) {
return notModified();
}
`#### JavaScript
Helper function to create a JavaScript file response with any header.
This is useful to create JS files based on data inside a Resource Route.
`ts
import { javascript } from "remix-utils/responses";export async function loader({ request }: Route.LoaderArgs) {
return javascript("console.log('Hello World')");
}
`#### Stylesheet
Helper function to create a CSS file response with any header.
This is useful to create CSS files based on data inside a Resource Route.
`ts
import { stylesheet } from "remix-utils/responses";export async function loader({ request }: Route.LoaderArgs) {
return stylesheet("body { color: red; }");
}
`#### PDF
Helper function to create a PDF file response with any header.
This is useful to create PDF files based on data inside a Resource Route.
`ts
import { pdf } from "remix-utils/responses";export async function loader({ request }: Route.LoaderArgs) {
return pdf(await generatePDF(request.formData()));
}
`#### HTML
Helper function to create a HTML file response with any header.
This is useful to create HTML files based on data inside a Resource Route.
`ts
import { html } from "remix-utils/responses";export async function loader({ request }: Route.LoaderArgs) {
return html("
Hello World
");
}
`#### XML
Helper function to create a XML file response with any header.
This is useful to create XML files based on data inside a Resource Route.
`ts
import { xml } from "remix-utils/responses";export async function loader({ request }: Route.LoaderArgs) {
return xml(" ");
}
`#### Plain Text
Helper function to create a TXT file response with any header.
This is useful to create TXT files based on data inside a Resource Route.
`ts
import { txt } from "remix-utils/responses";export async function loader({ request }: Route.LoaderArgs) {
return txt(
);
}
`$3
> [!NOTE]
> This depends on
@standard-schema/spec, and React Router.Cookie objects in Remix allows any type, the typed cookies from Remix Utils lets you use any Standard Schema compatible library to parse the cookie values and ensure they conform to a schema.
`ts
import { createCookie } from "react-router";
import { createTypedCookie } from "remix-utils/typed-cookie";
import { z } from "zod"; //or another Standard Schema compatible librarylet cookie = createCookie("returnTo", cookieOptions);
// I recommend you to always add
nullable to your schema, if a cookie didn't
// come with the request Cookie header Remix will return null, and it can be
// useful to remove it later when clearing the cookie
let schema = z.string().url().nullable();// pass the cookie and the schema
let typedCookie = createTypedCookie({ cookie, schema });
// this will be a string and also a URL
let returnTo = await typedCookie.parse(request.headers.get("Cookie"));
// this will not pass the schema validation and throw a ZodError
await typedCookie.serialize("a random string that's not a URL");
// this will make TS yell because it's not a string, if you ignore it it will
// throw a ZodError
await typedCookie.serialize(123);
`You could also use typed cookies with any sessionStorage mechanism from Remix.
`ts
let cookie = createCookie("session", cookieOptions);
let schema = z.object({ token: z.string().nullish() }).nullable();let sessionStorage = createCookieSessionStorage({
cookie: createTypedCookie({ cookie, schema }),
});
// if this works then the correct data is stored in the session
let session = sessionStorage.getSession(request.headers.get("Cookie"));
session.unset("token"); // remove a required key from the session
// this will throw a ZodError because the session is missing the required key
await sessionStorage.commitSession(session);
`Now Zod will ensure the data you try to save to the session is valid removing any extra field and throwing if you don't set the correct data in the session.
> [!IMPORTANT]
> The session object is not really typed so doing session.get will not return the correct type, you can do
schema.parse(session.data) to get the typed version of the session data.You can also use async refinements in your schemas because typed cookies uses parseAsync method from Zod.
`ts
let cookie = createCookie("session", cookieOptions);let schema = z
.object({
token: z.string().refine(async (token) => {
let user = await getUserByToken(token);
return user !== null;
}, "INVALID_TOKEN"),
})
.nullable();
let sessionTypedCookie = createTypedCookie({ cookie, schema });
// this will throw if the token stored in the cookie is not valid anymore
sessionTypedCookie.parse(request.headers.get("Cookie"));
`Finally, to be able to delete a cookie, you can add
.nullable() to your schema and serialize it with null as value.`ts
// Set the value as null and expires as current date - 1 second so the browser expires the cookie
await typedCookie.serialize(null, { expires: new Date(Date.now() - 1) });
`If you didn't add
.nullable() to your schema, you will need to provide a mock value and set the expires date to the past.`ts
let cookie = createCookie("returnTo", cookieOptions);
let schema = z.string().url().nullable();let typedCookie = createTypedCookie({ cookie, schema });
await typedCookie.serialize("some fake url to pass schema validation", {
expires: new Date(Date.now() - 1),
});
`$3
> [!NOTE]
> This depends on
react.Server-Sent Events are a way to send data from the server to the client without the need for the client to request it. This is useful for things like chat applications, live updates, and more.
There are two utils provided to help with the usage inside Remix:
-
eventStream
- useEventSourceThe
eventStream function is used to create a new event stream response needed to send events to the client. This must live in a Resource Route.`ts
// app/routes/sse.time.ts
import { eventStream } from "remix-utils/sse/server";export async function loader({ request }: Route.LoaderArgs) {
return eventStream(request.signal, function setup(send) {
let intervalId = setInterval(() => {
send({ event: "time", data: new Date().toISOString() });
}, 1000);
return () => void clearInterval(intervalId); // Cleanup function
});
}
`Then, inside any component, you can use the
useEventSource hook to connect to the event stream.`tsx
// app/components/counter.ts
import { useEventSource } from "remix-utils/sse/react";function Counter() {
// Here
/sse/time is the resource route returning an eventStream response
let time = useEventSource("/sse/time", { event: "time" }); if (!time) return null;
return (
);
}
`The
event name in both the event stream and the hook is optional, in which case it will default to message, if defined you must use the same event name in both sides, this also allows you to emit different events from the same event stream.For Server-Sent Events to work, your server must support HTTP streaming. If you don't get SSE to work check if your deployment platform has support for it.
Because SSE count towards the limit of HTTP connections per domain, the
useEventSource hook keeps a global map of connections based on the provided URL and options. As long as they are the same, the hook will open a single SSE connection and share it between instances of the hook.Once there are no more instances of the hook re-using a connection, it will be closed and removed from the map.
You can use the
component to control the map.`tsx
let map: EventSourceMap = new Map();
return (
);
`This way, you can overwrite the map with a new one for a specific part of your app. Note that this provider is optional and a default map will be used if you don't provide one.
$3
> [!NOTE]
> This depends on
zod, and React Router.Rolling cookies allows you to prolong the expiration of a cookie by updating the expiration date of every cookie.
The
rollingCookie function is prepared to be used in entry.server exported function to update the expiration date of a cookie if no loader set it.For document request you can use it on the
handleRequest function:`ts
import { rollingCookie } from "remix-utils/rolling-cookie";import { sessionCookie } from "~/session.server";
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
await rollingCookie(sessionCookie, request, responseHeaders);
return isbot(request.headers.get("user-agent"))
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
);
}
`And for data request you can do it on the
handleDataRequest function:`ts
import { rollingCookie } from "remix-utils/rolling-cookie";export let handleDataRequest: HandleDataRequestFunction = async (
response: Response,
{ request },
) => {
let cookieValue = await sessionCookie.parse(
responseHeaders.get("set-cookie"),
);
if (!cookieValue) {
cookieValue = await sessionCookie.parse(request.headers.get("cookie"));
responseHeaders.append(
"Set-Cookie",
await sessionCookie.serialize(cookieValue),
);
}
return response;
};
`> [!NOTE] > Read more about rolling cookies in Remix.
$3
> [!NOTE]
> This depends on React Router.
It's common to need to handle more than one action in the same route, there are many options here like sending the form to a resource route or using an action reducer, the
namedAction function uses some conventions to implement the action reducer pattern.`tsx
import { namedAction } from "remix-utils/named-action";export async function action({ request }: Route.ActionArgs) {
return namedAction(await request.formData(), {
async create() {
// do create
},
async update() {
// do update
},
async delete() {
// do delete
},
});
}
export default function Component() {
return (
<>
>
);
}
`This function can follow this convention:
You can pass a FormData object to the
namedAction, then it will try to find a field named intent and use the value as the action name.If, in any case, the action name is not found, the
actionName then the library will try to call an action named default, similar to a switch in JavaScript.If the
default is not defined it will throw a ReferenceError with the message Action "${name}" not found.If the library couldn't found the name at all, it will throw a ReferenceError with the message
Action name not found$3
> [!CAUTION]
> This can potentialy create big
Link header and can cause extremely hard to debug issues. Some provider's load balancers have set certain buffer for parsing outgoing response's headers and thanks to preloadRouteAssets you can easily reach that in a medium sized application.
> Your load balancer can randomly stop responding or start throwing 502 error.
> To overcome this either don't use preloadRouteAssets, set bigger buffer for processing response headers if you own the loadbalancer or use the experimentalMinChunkSize option in Vite config (this does not solve the issue permanently, only delays it)The
Link header allows responses to push to the browser assets that are needed for the document, this is useful to improve the performance of the application by sending those assets earlier.Once Early Hints is supported this will also allows you to send the assets even before the document is ready, but for now you can benefit to send assets to preload before the browser parse the HTML.
You can do this with the functions
preloadRouteAssets, preloadLinkedAssets and preloadModuleAssets.All functions follows the same signature:
`ts
import {
preloadRouteAssets,
preloadLinkedAssets,
preloadModuleAssets,
} from "remix-utils/preload-route-assets";// entry.server.tsx
export default function handleRequest(
request: Request,
statusCode: number,
headers: Headers,
context: EntryContext,
) {
let markup = renderToString(
,
);
headers.set("Content-Type", "text/html");
preloadRouteAssets(context, headers); // add this line
// preloadLinkedAssets(context, headers);
// preloadModuleAssets(context, headers);
return new Response("" + markup, {
status: statusCode,
headers: headers,
});
}
`The
preloadRouteAssets is a combination of both preloadLinkedAssets and preloadModuleAssets so you can use it to preload all assets for a route, if you use this one you don't need the other twoThe
preloadLinkedAssets function will preload any link with rel: "preload" added with the Remix's LinkFunction, so you can configure assets to preload in your route and send them in the headers automatically. It will additionally preload any linked stylesheet file (with rel: "stylesheet") even if not preloaded so it will load faster.The
preloadModuleAssets function will preload all the JS files Remix adds to the page when hydrating it, Remix already renders a for each now before the