• Stars
    star
    1,934
  • Rank 23,958 (Top 0.5 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 3 years ago
  • Updated 7 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A set of utility functions and types to use with Remix.run

Remix Utils

This package contains simple utility functions to use with Remix.run.

Installation

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.

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.

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.

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.

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

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:

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.

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.

Note If you're using React 18 and a streaming server rendering API (eg. renderToPipeableStream) you probably want to use a <Suspense> boundary instead.

export default function Component() {
  return (
    <Suspense fallback={<SimplerStaticVersion />}>
      <ComplexComponentNeedingBrowserEnvironment />
    </Suspense>
  );
}

See "Providing a fallback for server errors and server-only content" in the React Suspense docs.

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.

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.

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:

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.

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.

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

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<string | RegExp>: 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.

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.

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.

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.

<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.

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.

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 <link /> 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.

// create the dynamicLinks function with the correct type
// note: loader type is optional
let dynamicLinks: DynamicLinksFunction<SerializeFrom<typeof loader>> = ({
  id,
  data,
  params,
  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.

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 LinksFunctions.

Note 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:

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  future: { v2_meta: true },
};

Then you can use it like this:

export let meta: V2_MetaFunction<typeof loader> = ({ data }) => {
  if (!data.user) return [];
  return [
    { tagName: "link", rel: "preload", href: data.user.avatar, as: "image" },
  ];
};

ExternalScripts

If you need to load different external scripts on certain routes, you can use the ExternalScripts component together with the ExternalScriptsFunction type.

In the route you want to load the script add a handle export with a scripts method, this method should implement the ExternalScriptsFunction type.

// create the scripts function with the correct type
// note: loader type is optional
let scripts: ExternalScriptsFunction<SerializeFrom<typeof loader>> = ({
  id,
  data,
  params,
  location,
  parentsData,
}) => {
  return [
    {
      src: "https://code.jquery.com/jquery-3.6.0.min.js",
      integrity: "sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=",
      crossOrigin: "anonymous",
    },
  ];
};

// and export it through the handle, you could also create it inline here
// if you don't care about the type
export let handle = { scripts };

Then, in the root route, add the ExternalScripts component together with the Remix's Scripts component, usually inside a Document component.

import { Links, LiveReload, Meta, Scripts, ScrollRestoration } from "remix";
import { ExternalScripts } 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 />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <ExternalScripts />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

Now, any script you defined in the ScriptsFunction will be added to the HTML together with a <link rel="preload"> before it.

Tip: You could use it 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.

StructuredData

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 include structured data (JSON-LD) scripts on certain routes, you can use the StructuredData component together with the HandleStructuredData type or StructuredDataFunction type.

In the route you want to include the structured data, add a handle export with a structuredData method, this method should implement the StructuredDataFunction type.

import type { WithContext, BlogPosting } from "schema-dts";

// create the structuredData function with the correct type
// note: loader type is optional
let structuredData: StructuredDataFunction<
  SerializeFrom<typeof loader>,
  BlogPosting
> = ({ id, data, params, location, parentsData }) => {
  let { post } = data;

  return {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    datePublished: post.published,
    mainEntityOfPage: {
      "@type": "WebPage",
      "@id": post.postUrl,
    },
    image: post.featuredImage,
    author: {
      "@type": "Person",
      name: post.authorName,
    },
  };
};

// and export it through the handle, you could also create it inline here
// if you don't care about the type or using the `HandleStructuredData` type
export let handle = { structuredData };

Then, in the root route, add the StructuredData component together with the Remix's Scripts component, usually inside a Document component.

import { Links, LiveReload, Meta, Scripts, ScrollRestoration } from "remix";
import { StructuredData } 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 />
        <Links />
        <StructuredData />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

Now, any structured data you defined in the StructuredDataFunction will be added to the HTML, in the head. You may choose to include the <StructuredData /> in either the head or the body, both are valid.

If you want to upgrade to use the V2_MetaFunction, first enable it in your Remix app:

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  future: { v2_meta: true },
};

Then you can use it like this:

export let meta: V2_MetaFunction<typeof loader> = ({ data }) => {
  let { post } = data;
  return [
    {
      "script:ld+json": {
        "@context": "https://schema.org",
        "@type": "BlogPosting",
        datePublished: post.published,
        mainEntityOfPage: { "@type": "WebPage", "@id": post.postUrl },
        image: post.featuredImage,
        author: { "@type": "Person", name: post.authorName },
      },
    },
  ];
};

useGlobalTransitionStates

This hook lets you know if the value of transition.state and every fetcher.state in the app.

import { useGlobalTransitionStates } from "remix-utils";

export function GlobalPendingUI() {
  let states = useGlobalTransitionStates();

  if (state.includes("loading")) {
    // The app is loading.
  }

  if (state.includes("submitting")) {
    // The app is submitting.
  }

  // The app is idle
}

The return value of useGlobalTransitionStates 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).

useGlobalPendingState

This hook lets you know if the global transition or if one of any active fetchers is either loading or submitting.

import { useGlobalPendingState } from "remix-utils";

export function GlobalPendingUI() {
  let globalState = useGlobalPendingState();

  if (globalState === "idle") return null;
  return <Spinner />;
}

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.

useGlobalSubmittingState

This hook lets you know if the global transition or if one of any active fetchers is submitting.

import { useGlobalSubmittingState } from "remix-utils";

export function GlobalPendingUI() {
  let globalState = useGlobalSubmittingState();

  if (globalState === "idle") return null;
  return <Spinner />;
}

The return value of useGlobalSubmittingState is either "idle" or "submitting".

useGlobalLoadingState

This hook lets you know if the global transition or if one of any active fetchers is loading.

import { useGlobalLoadingState } from "remix-utils";

export function GlobalPendingUI() {
  let globalState = useGlobalLoadingState();

  if (globalState === "idle") return null;
  return <Spinner />;
}

The return value of useGlobalLoadingState is either "idle" or "loading".

useHydrated

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.

import { useHydrated } from "remix-utils";

export function Component() {
  let isHydrated = useHydrated();

  if (isHydrated) {
    return <ClientOnlyComponent />;
  }

  return <ServerFallback />;
}

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.

useLocales

This hooks lets you get the locales returned by the root loader. It follows a simple convention, your root loader return value should be an objet 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.

// in the root loader
export async function loader({ request }: 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 <time dateTime={dateTime}>{formattedDate}</time>;
}

The return type of useLocales is ready to be used with the Intl API.

useShouldHydrate

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.

import type { ReactNode } from "react";
import { Links, LiveReload, Meta, Scripts } from "@remix-run/react";
import { useShouldHydrate } from "remix-utils";

interface DocumentProps {
  children: ReactNode;
  title?: string;
}

export function Document({ children, title }: DocumentProps) {
  let shouldHydrate = useShouldHydrate();
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <link rel="icon" href="/favicon.png" type="image/png" />
        {title ? <title>{title}</title> : null}
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        {shouldHydrate && <Scripts />}
        <LiveReload />
      </body>
    </html>
  );
}

Now, you can export a handle object with the hydrate property as true in any route module.

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.

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.

getClientIPAddress

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.

export async function loader({ request }: LoaderArgs) {
  // using the request
  let ipAddress = getClientIPAddress(request);
  // or using the headers
  let ipAddress = getClientIPAddress(request.headers);
}

If it can't find he ipAddress 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
  • 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.

getClientLocales

This function let you get the locales of the client (the user) who originated the request.

export async function loader({ request }: 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.

import { getClientLocales } from "remix-utils";
export async function loader({ request }: 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.

isPrefetch

This function let you identify if a request was created because of a prefetch triggered by using <Link prefetch="intent"> or <Link prefetch="render">.

This will let you implement a short cache only for prefetch requests so you avoid the double data request.

export async function loader({ request }: 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 });
}

Responses

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.

import { redirectBack } from "remix-utils";

export async function action({ request }: ActionArgs) {
  return 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.

Created

Helper function to create a Created (201) response with a JSON body.

import { created } from "remix-utils";

export async function action({ request }: ActionArgs) {
  let result = await doSomething(request);
  return created(result);
}

Bad Request

Helper function to create a Bad Request (400) response with a JSON body.

import { badRequest } from "remix-utils";

export async function action() {
  throw badRequest({ message: "You forgot something in the form." });
}

Unauthorized

Helper function to create an Unauthorized (401) response with a JSON body.

import { unauthorized } from "remix-utils";

export async function loader() {
  // usually what you really want is to throw a redirect to the login page
  throw unauthorized({ message: "You need to login." });
}

Forbidden

Helper function to create a Forbidden (403) response with a JSON body.

import { forbidden } from "remix-utils";

export async function loader() {
  throw forbidden({ message: "You don't have access for this." });
}

Not Found

Helper function to create a Not Found (404) response with a JSON body.

import { notFound } from "remix-utils";

export async function loader() {
  throw notFound({ message: "This doesn't exist." });
}

Unprocessable Entity

Helper function to create an Unprocessable Entity (422) response with a JSON body.

import { unprocessableEntity } from "remix-utils";

export async function loader() {
  throw unprocessableEntity({ message: "This doesn't exists." });
}

This is used by the CSRF validation. You probably don't want to use it directly.

Server Error

Helper function to create a Server Error (500) response with a JSON body.

import { serverError } from "remix-utils";

export async function loader() {
  throw serverError({ message: "Something unexpected happened." });
}

Not Modified

Helper function to create a Not Modified (304) response without a body and any header.

export async function loader({ request }: 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.

export async function loader({ request }: 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.

export async function loader({ request }: 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.

export async function loader({ request }: 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.

export async function loader({ request }: LoaderArgs) {
  return html("<h1>Hello World</h1>");
}

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.

export async function loader({ request }: LoaderArgs) {
  return xml("<?xml version='1.0'?><catalog></catalog>");
}

TXT

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.

export async function loader({ request }: LoaderArgs) {
  return txt(`
    User-agent: *
    Allow: /
  `);
}

Typed Cookies

Cookie objects in Remix allows any type, the typed cookies from Remix Utils lets you use Zod to parse the cookie values and ensure they conform to a schema.

import { createCookie } from "@remix-run/node";
import { createTypedCookie } from "remix-utils";
import { z } from "zod";

let cookie = createCookie("returnTo", cookieOptions);
let schema = z.string().url();

// 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 cookie.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 cookie.serialize(123);

You could also use typed cookies with any sessionStorage mechanism from Remix.

let cookie = createCookie("session", cookieOptions);
let schema = z.object({ token: z.string() });

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.

Note 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.

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"),
});

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"));

Typed Sessions

Session objects in Remix allows any type, the typed sessions from Remix Utils lets you use Zod to parse the session data and ensure they conform to a schema.

import { createCookieSessionStorage } from "@remix-run/node";
import { createTypedSessionStorage } from "remix-utils";
import { z } from "zod";

let schema = z.object({
  token: z.string().optional(),
  count: z.number().default(1),
});

// you can use a Remix's Cookie container or a Remix Utils's Typed Cookie container
let sessionStorage = createCookieSessionStorage({ cookie });

// pass the session storage and the schema
let typedSessionStorage = createTypedSessionStorage({ sessionStorage, schema });

Now you can use typedSessionStorage as a drop-in replacement for your normal sessionStorage.

let session = typedSessionStorage.getSession(request.headers.get("Cookie"));

session.get("token"); // this will be a string or undefined
session.get("count"); // this will be a number
session.get("random"); // this will make TS yell because it's not in the schema

session.has("token"); // this will be a boolean
session.has("count"); // this will be a boolean

// this will make TS yell because it's not a string, if you ignore it it will
// throw a ZodError
session.set("token", 123);

Now Zod will ensure the data you try to save to the session is valid by not allowing you to get, set or unset data.

Note Remember that you either need to mark fields as optional or set a default value in the schema, otherwise it will be impossible to call getSession to get a new session object.

You can also use async refinements in your schemas because typed sesions uses parseAsync method from Zod.

let schema = z.object({
  token: z
    .string()
    .optional()
    .refine(async (token) => {
      if (!token) return true; // handle optionallity
      let user = await getUserByToken(token);
      return user !== null;
    }, "INVALID_TOKEN"),
});

let typedSessionStorage = createTypedSessionStorage({ sessionStorage, schema });

// this will throw if the token stored in the session is not valid anymore
typedSessionStorage.getSession(request.headers.get("Cookie"));

Server-Sent Events

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
  • useEventSource

The eventStream function is used to create a new event stream response needed to send events to the client.

// app/routes/sse.time.ts
import { eventStream } from "remix-utils";

export async function loader({ request }: LoaderArgs) {
  return eventStream(request.signal, function setup(send) {
    let timer = setInterval(() => {
      send({ event: "time", data: new Date().toISOString() });
    }, 1000);

    return function clear() {
      clearInterval(timer);
    };
  });
}

Then, inside any component, you can use the useEventSource hook to connect to the event stream.

// app/components/counter.ts
import { useEventSource } from "remix-utils";

function Counter() {
  let time = useEventSource("/sse/time", { event: "time" });

  if (!time) return null;

  return (
    <time dateTime={time}>
      {new Date(time).toLocaleTimeString("en", {
        minute: "2-digit",
        second: "2-digit",
        hour: "2-digit",
      })}
    </time>
  );
}

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.

Rolling Cookies

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:

import { rollingCookie } from "remix-utils";

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:

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.

Named actions

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.

import { namedAction } from "remix-utils";

export async function action({ request }: ActionArgs) {
  return namedAction(request, {
    async create() {
      // do create
    },
    async update() {
      // do update
    },
    async delete() {
      // do delete
    },
  });
}

export default function Component() {
  return (
    <>
      <Form method="post" action="?/create">
        ...
      </Form>

      <Form method="post" action="?/update">
        ...
      </Form>

      <Form method="post" action="?/delete">
        ...
      </Form>
    </>
  );
}

This function can follow many conventions

You can pass a FormData object to the namedAction, then it will try to

  • Find a field named /something and use it as the action name removing the /
  • Find a field named intent and use the value as the action name
  • Find a field named action and use the value as the action name
  • Find a field named _action and use the value as the action name

You can pass an URLSearchParams object to the namedAction, then it will try to

  • Find a query parameter named /something and use it as the action name removing the /
  • Find a query parameter named intent and use the value as the action name
  • Find a query parameter named action and use the value as the action name
  • Find a query parameter named _action and use the value as the action name

You can pass an URL object to the namedAction, it will behave as with a URLSearchParams object.

You can pass a Request object to the namedAction, then it will try to

  • Call new URL(request.url) and use it as the URL object
  • Call request.formData() and use it as the FormData object

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

Preload Route Assets

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:

// entry.server.tsx
export default function handleRequest(
  request: Request,
  statusCode: number,
  headers: Headers,
  context: EntryContext
) {
  let markup = renderToString(
    <RemixServer context={context} url={request.url} />
  );
  headers.set("Content-Type", "text/html");

  preloadRouteAssets(context, headers); // add this line
  // preloadLinkedAssets(context, headers);
  // preloadModuleAssets(context, headers);

  return new Response("<!DOCTYPE html>" + 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 two

The 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 <link rel="modulepreload"> for each now before the <script type="module"> used to start the application, this will use Link headers to preload those assets.

Safe Redirects

When performing a redirect, if the URL is user provided we can't trust it, if you do you're opening a vulnerability to phishing scam by allowing bad actors to redirect the user to malicious websites.

https://remix.utills/?redirectTo=https://malicious.app

To help you prevent this Remix Utils gives you a safeRedirect function which can be used to check if the URL is "safe".

Note: In this context, safe means the URL starts with / but not //, this means the URL is a pathname inside the same app and not an external link.

export async function loader({ request }: LoaderArgs) {
  let { searchParams } = new URL(request.url);
  let redirectTo = searchParams.get("redirectTo");
  return redirect(safeRedirect(redirectTo, "/home"));
}

The second argumento of safeRedirect is the default redirect which by when not configured is /, this lets you tell safeRedirect where to redirect the user if the value is not safe.

JSON Hash Response

When returning a json from a loader function, you may need to get data from different DB queries or API requests, typically you would something like this

export async function loader({ params }: LoaderData) {
  let postId = z.string().parse(params.postId);
  let [post, comments] = await Promise.all([getPost(), getComments()]);
  return json({ post, comments });

  async function getPost() {
    /* … */
  }
  async function getComments() {
    /* … */
  }
}

The jsonHash function lets you define those functions directly in the json, reducing the need to create extra functions and variables.

export async function loader({ params }: LoaderData) {
  let postId = z.string().parse(params.postId);
  return jsonHash({
    async post() {
      // Implement me
    },
    async comments() {
      // Implement me
    },
  });
}

It also calls your functions using Promise.all so you can be sure the data is retrieved in parallel.

Additionally, you can pass non-async functions, values and promises.

export async function loader({ params }: LoaderData) {
  let postId = z.string().parse(params.postId);
  return jsonHash({
    postId, // value
    comments: getComments(), // Promise
    slug() {
      // Non-async function
      return postId.split("-").at(1); // get slug from postId param
    },
    async post() {
      // Async function
      // Implement me
    },
  });

  async function getComments() {
    /* … */
  }
}

The result of jsonHash is a TypedResponse and it's correctly typed so using it with typeof loader works flawlessly.

export default function Component() {
  // all correctly typed
  let { postId, comments, slug, post } = useLoaderData<typeof loader>();

  // more code…
}

Delegate Anchors to Remix

When using Remix, you can use the <Link> component to navigate between pages. However, if you have a <a href> that links to a page in your app, it will cause a full page refresh. This can be what you want, but sometimes you want to use client-side navigation here instead.

The useDelegatedAnchors hook lets you add client-side navigation to anchor tags in a portion of your app. This can be specially useful when working with dynamic content like HTML or Markdown from a CMS.

import { useDelegatedAnchors } from "remix-utils";

export async function loader() {
  let content = await fetchContentFromCMS();
  return json({ content });
}

export default function Component() {
  let { content } = useLoaderData<typeof loader>();

  let ref = useRef<HTMLDivElement>(null);
  useDelegatedAnchors(ref);

  return <article ref={ref} dangerouslySetInnerHTML={{ __html: content }} />;
}

Prefetch Anchors

If additionally you want to be able to prefetch your anchors you can use the PrefetchPageAnchors components.

This components wraps your content with anchors inside, it detects any hovered anchor to prefetch it, and it delegates them to Remix.

import { PrefetchPageAnchors } from "remix-utils";

export async function loader() {
  let content = await fetchContentFromCMS();
  return json({ content });
}

export default function Component() {
  let { content } = useLoaderData<typeof loader>();

  return (
    <PrefetchPageAnchors>
      <article ref={ref} dangerouslySetInnerHTML={{ __html: content }} />
    </PrefetchPageAnchors>
  );
}

Now you can see in your DevTools that when the user hovers an anchor it will prefetch it, and when the user clicks it will do a client-side navigation.

Author

License

  • MIT License

More Repositories

1

remix-auth

Simple Authentication for Remix
TypeScript
1,782
star
2

remix-i18next

The easiest way to translate your Remix apps
TypeScript
520
star
3

impresionante-javascript

Recopilación de links en español sobre JavaScript
415
star
4

flagged

Feature Flags for React made easy with hooks, HOC and Render Props
TypeScript
386
star
5

remix-hono

Hono middlewares for Remix
TypeScript
321
star
6

next-ga

Next.js HOC to integrate Google Analytics on every page change
JavaScript
232
star
7

grial

A Node.js framework for creating GraphQL API servers easily and without a lot of boilerplate.
JavaScript
189
star
8

next-nprogress

Next.js HOC to integrate NProgress inside your app
JavaScript
149
star
9

remix-auth-oauth2

A OAuth2Strategy for Remix Auth
TypeScript
129
star
10

use-mutation

🧬 Run side-effects safely in React
TypeScript
119
star
11

swr-sync-storage

Synchronize SWR cache with localStorage or sessionStorage to get offline cache
TypeScript
109
star
12

react-lazy-image

Component to render images and lazyload them if are in the viewport (or near to them).
JavaScript
101
star
13

personal-site

Personal website
TypeScript
95
star
14

now-storage

Use Now static deployments to upload and store files.
JavaScript
90
star
15

remix-auth-form

A Remix Auth strategy for working with forms.
TypeScript
79
star
16

sergiodxa.com

The code behind sergiodxa.com
TypeScript
71
star
17

remix-auth-github

A GitHubStrategy for Remix Auth, based on the OAuth2Strategy
TypeScript
66
star
18

redux-in-spanish

Traducción al español de la documentación de Redux.
64
star
19

web-oidc

An OpenID Connect client built using only Web APIs
TypeScript
49
star
20

remix-auth-strategy-template

A template for creating a new Remix Auth strategy.
TypeScript
47
star
21

remix-socket.io

A Remix app using Express and Socket.io
TypeScript
45
star
22

next-socket.io

An example app with Next.js and socket.io
JavaScript
38
star
23

remix-inspector

Remix devtools to inspect your route data.
TypeScript
38
star
24

react-wordpress

Example of a React+Next.js+WordPress application.
JavaScript
36
star
25

react-lazy-memo

CRA with Suspense, lazy and memo usage demo app
JavaScript
36
star
26

swr-mutate-many

Little function to call mutate against multiple cached keys of SWR
TypeScript
26
star
27

remix-demo-infinite-scroll

A collection of infinite scroll pagination demos built with Remix
TypeScript
26
star
28

now-ab

A simple proxy server to handle Now.sh deployments AB tests
JavaScript
25
star
29

remix-on-bun

TypeScript
25
star
30

es6

Material para la clase BONUS en Comunidad Platzi sobre ECMAScript 6.
HTML
23
star
31

react-course-project

Proyecto para el curso de React.js
JavaScript
22
star
32

micro-next

Integrations between Micro and Next.js
JavaScript
20
star
33

collected-notes

A TypeScript client for the Collected Notes API
17
star
34

now-parcel

A Now v2 Parcel builder.
JavaScript
16
star
35

api-client

A strongly typed API client using Zod
TypeScript
15
star
36

remix-demo-file-upload

A simple demo on how to add a file upload that shows the image being uploaded and later replace it with the actual one
TypeScript
15
star
37

remix-vite-i18next

An example Remix + Vite app with remix-i18next setup
TypeScript
14
star
38

collected-remix

A Collected Notes client in Remix
TypeScript
12
star
39

micro-platzi-profile

Ejemplo de microservicio usando https://github.com/zeit/micro para scrappear un perfíl de Platzi
JavaScript
12
star
40

markdown-it-mentions

markdown-it plugin to support Twitter like mentions
JavaScript
11
star
41

react-use-permissions

React hook for Permissions API
JavaScript
11
star
42

use-safe-callback

Wrap a function to ensure you never call it if a component is unmounted, useful to avoid cases where an async code could finish after a component has unmounted and it tries to update a state
TypeScript
11
star
43

redux-duck

Helper function to create Redux modules using the ducks-modular-redux proposal
TypeScript
10
star
44

use-log

Log a state or prop every time it changes
TypeScript
10
star
45

cf-bootcamp-react-router-lesson

TypeScript
9
star
46

react-simple-modal

React modal make it simple.
JavaScript
8
star
47

collected-notes-website

A Next.js based clon of the Collected Notes website
TypeScript
8
star
48

remix-mdn

A MDN clone of Remix
TypeScript
8
star
49

remix-auth-token

Token based authentication for Remix Auth
TypeScript
8
star
50

next-analytics

Next.js HOC to integrate Google Analytics and FB Pixel
JavaScript
8
star
51

personal-slides

The slides of my talks
JavaScript
7
star
52

collected-notes-next-blog

Blog example using Collected Notes as CMS
TypeScript
7
star
53

react-use-quicklink

Quicklink hook for React
JavaScript
6
star
54

use-validate-image-url

TypeScript
6
star
55

email-value

TypeScript
6
star
56

isomorphic-react-todo

Aplicación web de TODOs isomórfica hecha con React.js
JavaScript
6
star
57

use-consistent-value

Keep a consistent reference to an object or array based on their values.
TypeScript
6
star
58

markdown-it-codesandbox

markdown-it plugin to embed CodeSandbox editor.
JavaScript
5
star
59

remix-demo-prefetch-fetcher

Demo Remix app to prefetch the fetcher data
TypeScript
5
star
60

next-custom-query

Example Next.js app using a custom server with queries in the URLs.
JavaScript
5
star
61

yifi-search

Backbone.js WebApp for search torrents with the YIFI's API.
JavaScript
5
star
62

navBarAdaptable-mejorUX

Barra de navegación adaptable con Media Queries y JS
CSS
5
star
63

next-babel-minify

Next.js plugin to replace UglifyJS for BabelMinify
JavaScript
5
star
64

gulp-tasks-boilerplate

Plantilla de tareas de Gulp.js para distintos usos, con un package.json con todos los módulos requeridos.
JavaScript
5
star
65

remix-auth-webauthn

A strategy to support WebAuthn
TypeScript
4
star
66

remix-auth-austin-demo

Remix Auth demo for the Remix Austin meetup
TypeScript
4
star
67

MarvelDB

Webapp to find characters in the Marvel database.
JavaScript
4
star
68

remix-markdoc-example

TypeScript
4
star
69

remix-define-routes

A DSL to define Remix routes with code
TypeScript
4
star
70

swr-sync-session

This is an example Next.js + SWR application using SWR revalidation on focus feature to sync session between tabs
JavaScript
4
star
71

dataset

A free to use API to get a lists of various common datasets such as languages, countries, etc.
JavaScript
3
star
72

next-credentials

Next.js example using Credential Management API
JavaScript
3
star
73

personal-cli

A CLI tool with commands for personal usage.
JavaScript
3
star
74

platzimusic

Proyecto para el diplomado de React en Platzi
JavaScript
3
star
75

cf-custom-hooks

Ejercicios de Custom Hooks para Código Facilito
TypeScript
3
star
76

personal-api

An API created for my personal site usage.
JavaScript
2
star
77

cf-bootcamp-deploy

TypeScript
2
star
78

techtalks-remix-demo

Demo of Remix.run for Tech Talks.pe
TypeScript
2
star
79

react-render-service

React Render as a Service
JavaScript
1
star
80

personal-shortening

A personal URL shortening service
JavaScript
1
star
81

virtual-event-starter-kit

TypeScript
1
star
82

remix-with-sentry-sdk

A reproduction of the issue https://github.com/getsentry/sentry-javascript/issues/6294
TypeScript
1
star
83

archive-it

API to get the messages of a Slack channel
TypeScript
1
star
84

ngCreate

CLI to create AngularJS files
JavaScript
1
star
85

react-chat-app

JavaScript
1
star
86

pokedex-swr

Pokedex Application using Next.js and SWR with Tailwind for styling
TypeScript
1
star
87

swr-devtools

TypeScript
1
star
88

dice-roller

A tiny library to calculate roll dices using the classic D&D sintax: 1d20+4.
JavaScript
1
star
89

react-i18n

Ejemplo de internacionalización con React.js y Format.js
JavaScript
1
star
90

es-query

An utilitarian library for DOM manipulation using ECMAScript 7 Function Bind Syntax
JavaScript
1
star
91

device-info

A little service to check a device information.
HTML
1
star
92

deno-md-to-html

Simple MD to HTML parser
TypeScript
1
star
93

package

A template to create new packages.
TypeScript
1
star
94

Check-userAgent

New method for JS window object that checks the userAgent and returns true if a mobile.
1
star
95

Elecalc

Webapp hecha con AngularJS para realizar distintos cálculos de electricidad de una manera fácil y rápida
HTML
1
star
96

tpb-app

WebApp in NodeJS to search torrents in The Pirate Bay
JavaScript
1
star
97

remix-island-demos

TypeScript
1
star