Skip to content

raf-effect-atom and effect-atom-boundary are on npm. Both are pre-1.0, so a minor release can still break them. Install them

raf-effect-atom

API reference

raf-effect-atom/react-router

Connects the loader verbs to React Router's Framework Mode.

A root middleware gives each server request its own registry, under a router context the app creates. Loaders start atoms with its verbs and return its dehydrate() with their data; the route component hydrates it with HydrationBoundary, inside the root's RegistryProvider, both from raf-effect-atom. A root clientMiddleware puts the browser's registry under the same context, so one function can be both the route's loader and its clientLoader: after the first page, loaders run in the browser, where ensure finds what the registry already holds. Nothing is imported from react-router.

Example

// app/atoms.ts
import { createContext } from "react-router"
import type { RequestAtoms } from "raf-effect-atom/react-router"

export const atomsContext = createContext<RequestAtoms>()

// app/root.tsx
import { atomsMiddleware } from "raf-effect-atom/react-router"

export const middleware = [atomsMiddleware(atomsContext)]
export const clientMiddleware = [clientAtomsMiddleware(atomsContext)]

// app/routes/issue.tsx
async function load(args: Route.LoaderArgs) {
  const atoms = args.context.get(atomsContext)
  const handle = await atoms.ensure(issue, Number(args.params.id))
  return { atoms: atoms.dehydrate(), issue: handle }
}

export const loader = load
export const clientLoader = load

export default function IssueRoute(props: Route.ComponentProps) {
  return (
    <HydrationBoundary state={props.loaderData.atoms}>
      <Issue issue={props.loaderData.issue} />
    </HydrationBoundary>
  )
}
import { atomsMiddleware, clientAtomsMiddleware } from "raf-effect-atom/react-router";

RequestAtoms

interfacemodelssince 0.1.0Source

interface RequestAtoms extends Loader.Loader {
  /** `Hydration.dehydrate` on the request's registry, for loader data. */
  readonly dehydrate: () => Array<EffectHydration.DehydratedAtom>;
  readonly registry: AtomRegistry.AtomRegistry;
}

One request's registry, with the loader verbs over it. In a client loader, the browser's registry, whose dehydrate returns nothing: its atoms are already where the components read them.

AtomsContext

interfacemodelssince 0.1.0Source

interface AtomsContext {
  readonly defaultValue?: RequestAtoms | undefined;
}

The router context atomsMiddleware sets: what react-router's createContext<RequestAtoms>() returns.

MiddlewareArgs

interfacemodelssince 0.1.0Source

interface MiddlewareArgs {
  readonly context: {
    readonly set: (context: AtomsContext, value: RequestAtoms) => void;
  };
  readonly request: Request;
}

The parts of React Router's middleware arguments atomsMiddleware uses.

AtomsMiddlewareOptions

interfacemodelssince 0.2.0Source

interface AtomsMiddlewareOptions {
  /**
   * Runs on each request's registry before the loaders, with the request,
   * to set what its atoms need, such as its cookie. The middleware waits
   * for one that returns a promise.
   */
  readonly seed?:
    | ((
        registry: AtomRegistry.AtomRegistry,
        request: Request,
      ) => void | Promise<void>)
    | undefined;
}

Options for atomsMiddleware.

atomsMiddleware

constintegrationssince 0.1.0Source

const atomsMiddleware: (
  context: AtomsContext,
  options?: AtomsMiddlewareOptions,
) => <Result>(
  args: MiddlewareArgs,
  next: () => Promise<Result>,
) => Promise<Result>;

Creates a root middleware that gives each server request its own registry, under the given router context.

Details

The registry is disposed once the response's body has been sent, or the client has gone: loader data streams its pending promises after the middleware returns, so the atoms behind them must keep running until then. When next doesn't return a Response, as Data Mode's query can, the registry is left to be collected with the request.

seed runs on the new registry before next, so every loader's atoms start with what it sets.

The verbs take the request's signal: when the client goes before the response, such as a navigation that leaves while its loaders wait, the ensure and fetch calls still waiting reject with its reason.

Gotchas

It's server middleware: export it as the root route's middleware, not clientMiddleware, which takes clientAtomsMiddleware.

Example

export const middleware = [
  atomsMiddleware(atomsContext, {
    // In-process RPC calls send the page request's cookie.
    seed: (registry, request) =>
      registry.set(requestCookie, request.headers.get("cookie") ?? undefined)
  })
]

clientAtomsMiddleware

constintegrationssince 0.2.1Source

const clientAtomsMiddleware: (
  context: AtomsContext,
) => <Result>(
  args: MiddlewareArgs,
  next: () => Promise<Result>,
) => Promise<Result>;

Creates a root client middleware that gives client loaders the browser's registry, under the context atomsMiddleware uses on the server.

When to use

Export it as the root route's clientMiddleware, and each route's loader function as both its loader and its clientLoader. The first page's loaders run on the server and stream their atoms in; after that, navigations run the loaders in the browser. ensure then renders what the browser's registry holds without a request, and a link's prefetch doesn't fetch a route's .data that the click would fetch again.

Details

The registry is the one the mounted RegistryProvider provides. Its dehydrate returns nothing, so a loader's data has the same shape in the browser and on the server, and HydrationBoundary has nothing to do. The verbs take the navigation's signal: when another navigation interrupts it, the ensure and fetch calls still waiting reject with its reason, and the atoms keep loading.

Gotchas

The atoms run in the browser, so their RPC protocol must reach the server from there, as it does for anything a component starts.

Example

// app/root.tsx
export const middleware = [atomsMiddleware(atomsContext)]
export const clientMiddleware = [clientAtomsMiddleware(atomsContext)]

// app/routes/issue.tsx: one function for both, since React Router
// removes `loader` from the browser's bundle.
export const loader = load
export const clientLoader = load