Remix Utils
This package contains simple utility functions to use with
Remix.run .
Installation
` ` ` bash npm install remix - utils ` ` `
API Reference
promiseHash
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 export async function loader ( { request } : 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 export async function loader ( { request } : LoaderArgs ) { return json ( await promiseHash ( { user : getUser ( request ) , posts : promiseHash ( { list : getPosts ( request ) , comments : promiseHash ( { list : getComments ( request ) , likes : getLikes ( request ) , } ) , } ) , } ) ) ; } ` ` `
timeout
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 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 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.
cacheAssets
This can only be run inside entry.client .
To use it, open your entry.client file and add this:
` ` ` ts import { cacheAssets } from "remix-utils" ; 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 cacheAssests ( { cacheName : "assets" , buildPath : "/build/" } ) .catch ( ( error ) => { // do something with the error, or not } ) ; ` ` `
ClientOnly
The ClientOnly component lets you render the children element only on the client-side, avoiding rendering it the server-side.
If you're using React 18 and a streaming server rendering API (eg. [renderToPipeableStream ](https://beta.reactjs.org/reference/react-dom/server/renderToPipeableStream)) you probably want to use a <Suspense> boundary instead.
>
export default function Component() {
<Suspense fallback={<SimplerStaticVersion />}>
<ComplexComponentNeedingBrowserEnvironment />
>
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" ; export default function Component ( ) { return ( < ClientOnly fallback = { < SimplerStaticVersion / > } > { ( ) => < ComplexComponentNeedingBrowserEnvironment / > } < / ClientOnly > ) ; } ` ` `
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.
ServerOnly
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" ; export default function Component ( ) { return ( < ServerOnly fallback = { < ComplexComponentNeedingBrowserEnvironment / > } > { ( ) => < SimplerStaticVersion / > } < / ServerOnly > ) ; } ` ` `
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 `
` HTML tag but it can work even if JS failed to load but it's enabled on the browser.
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.
CORS
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 export async function loader ( { request } : LoaderArgs ) { let data = await getData ( request ) ; let response = json < LoaderData > ( data ) ; return await cors ( request , response ) ; } ` ` `
You could also do the json and cors call in one line.
` ` ` ts export async function loader ( { request } : LoaderArgs ) { let data = await getData ( request ) ; return await cors ( request , json < LoaderData > ( data ) ) ; } ` ` `
And because cors mutates the response, you can also call it and later return.
` ` ` ts export async function loader ( { request } : LoaderArgs ) { let data = await getData ( request ) ; let response = json < LoaderData > ( data ) ; 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 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 ( < RemixServer context = { remixContext } url = { request .url } / > , { [ 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.
Possible values are:
- true : Enable CORS for any origin (same as "\*")
- false : Don't setup CORS
- string : Set to a specific origin, if set to "\*" it will allow any origin
- RegExp : Set to a RegExp to match against the origin
- `Array`: Set to an array of origins to match against the string or RegExp
- Function : Set to a function that will be called with the request origin
and should return a boolean indicating if the origin is allowed or not.
The default value is true .
- methods : Configures the Access-Control-Allow-Methods CORS header.
The default value is ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"] .
- 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.
CSRF
The CSRF related functions let you implement CSRF protection on your application.
This part of Remix Utils needs React and server-side code.
Generate the authenticity token
In the server, we need to add to our root component the following.
` ` ` ts import { createAuthenticityToken , json } from "remix-utils" ; import { getSession , commitSession } from "~/services/session.server" ; interface LoaderData { csrf : string ; } export async function loader ( { request } : LoaderArgs ) { let session = await getSession ( request .headers .get ( "cookie" ) ) ; let token = createAuthenticityToken ( session ) ; return json < LoaderData > ( { csrf : token } , { headers : { "Set-Cookie" : await commitSession ( session ) } } ) ; } ` ` `
The createAuthenticityToken function receives a session object and stores the authenticity token there using the csrf key (you can pass the key name as a second argument). Finally, you need to return the token in a json response and commit the session.
Render the AuthenticityTokenProvider
You need to read the authenticity token and render the AuthenticityTokenProvider component wrapping your code in your root.
` ` ` tsx import { Outlet , useLoaderData } from "@remix-run/react" ; import { Document } from "~/components/document" ; export default function Component ( ) { let { csrf } = useLoaderData < LoaderData > ( ) ; return ( < AuthenticityTokenProvider token = { csrf } > < Document > < Outlet / > < / Document > < / AuthenticityTokenProvider > ) ; } ` ` `
With this, your whole app can access the authenticity token generated in the root.
Rendering a Form
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 "@remix-run/react" ; import { AuthenticityTokenInput } from "remix-utils" ; export default function Component ( ) { return ( < Form method = "post" > < AuthenticityTokenInput / > < input type = "text" name = "something" / > < / Form > ) ; } ` ` `
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 < AuthenticityTokenInput name = "customName" / > ` ` `
You should only customize the name if you also changed it on createAuthenticityToken .
Alternative: Using useAuthenticityToken and useFetcher .
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 "remix" ; import { useAuthenticityToken } from "remix-utils" ; export function useMarkAsRead ( ) { let fetcher = useFetcher ( ) ; let csrf = useAuthenticityToken ( ) ; return function submit ( data ) { fetcher .submit ( { csrf , . . .data } , { action : "/action" , method : "post" } ) ; } ; } ` ` `
Verify in the Action
Finally, you need to verify the authenticity token in the action that received the request.
` ` ` ts import { verifyAuthenticityToken , redirectBack } from "remix-utils" ; import { getSession , commitSession } from "~/services/session.server" ; export async function action ( { request } : ActionArgs ) { let session = await getSession ( request .headers .get ( "Cookie" ) ) ; await verifyAuthenticityToken ( request , session ) ; // do something here return redirectBack ( request , { fallback : "/fallback" } ) ; } ` ` `
Suppose the authenticity token is missing on the session, the request body, or doesn't match. In that case, the function will throw an Unprocessable Entity response that you can either catch and handle manually or let pass and render your CatchBoundary.
DynamicLinks
Warning : Deprecated in favor of the V2_MetaFunction . This will be removed in the next major version. Check below for the new way to do this.
If you need to create ` ` tags based on the loader data instead of being static, you can use the `DynamicLinks` component together with the `DynamicLinksFunction` type.
In the route you want to define dynamic links add handle export with a dynamicLinks method, this method should implement the DynamicLinksFunction type.
` ` ` ts // create the dynamicLinks function with the correct type // note: loader type is optional let dynamicLinks : DynamicLinksFunction < SerializeFrom < typeof loader >> = ( { id , data , params , matches , location , parentsData , } ) => { if ( ! data .user ) return [] ; return [ { rel : "preload" , href : data .user .avatar , as : "image" } ] ; } ; // and export it through the handle, you could also create it inline here // if you don't care about the type export let handle = { dynamicLinks } ; ` ` `
Then, in the root route, add the DynamicLinks component before the Remix's Links component, usually inside a Document component.
` ` ` tsx import { Links , LiveReload , Meta , Scripts , ScrollRestoration } from "remix" ; import { DynamicLinks } from "remix-utils" ; type Props = { children : React .ReactNode ; title ? : string } ; export function Document ( { children , title } : Props ) { return ( < html lang = "en" > < head > < meta charSet = "utf-8" / > < meta name = "viewport" content = "width=device-width,initial-scale=1" / > { title ? < title > { title } < / title > : null } < Meta / > < DynamicLinks / > < Links / > < / head > < body > { children } < ScrollRestoration / > < Scripts / > < LiveReload / > < / body > < / html > ) ; } ` ` `
Now, any link you defined in the DynamicLinksFunction will be added to the HTML as any static link in your LinksFunction s.
You can also put the DynamicLinks after the Links component, it's up to you what to prioritize, since static links are probably prefetched when you do <Link prefetch> you may want to put the DynamicLinks first to prioritize them.
If you want to upgrade to use the V2_MetaFunction , first enable it in your Remix app:
` ` ` js /** @type {import('@remix-run/ dev ').AppConfig} */ module .exports = { future : { v2_meta : true } , } ; ` ` `
Then you can use it like this:
` ` ` ts export let meta : V2_MetaFunction < typeof loader > = ( { data } ) => { if ( ! data .user ) return [] ; return [ { tagName : "link" , rel : "preload" , href : data .user .avatar , as : "image" } , ] ; } ; ` ` `