Used
How toCreate a Per-Request Singleton with React Router Middleware
Create a Per-Request Singleton with React Router Middleware
If you're using the new middleware API in React Router v7.3.0, you can leverage this pattern to create a per-request singleton. This is useful for scenarios like per-request caching or batching multiple requests together.
Creating the Middleware
First, let's create a context and middleware that will generate a new singleton instance for each request:
This same middleware is part of Remix Utils so you don't need to copy it
import type { unstable_MiddlewareFunction, unstable_RouterContextProvider, } from "react-router"; import { unstable_createContext } from "react-router"; import type { Class } from "type-fest"; export function createSingletonMiddleware<T, A extends unknown[] = any[]>( options: createSingletonMiddleware.Options<T, A> ): createSingletonMiddleware.ReturnType<T> { let singletonContext = unstable_createContext<T | null>(null); return [ async function singletonMiddleware({ context }, next) { let instance = context.get(singletonContext); if (instance) return await next(); instance = new options.Class(...options.arguments); context.set(singletonContext, instance); return await next(); }, function getSingletonInstance(context) { let instance = context.get(singletonContext); if (!instance) throw new Error("Singleton instance not found"); return instance; }, ]; } export namespace createSingletonMiddleware { export interface Options<T, A extends unknown[] = any[]> { Class: Class<T, A>; arguments: A; } export type ReturnType<T> = [ unstable_MiddlewareFunction<unstable_RouterContextProvider>, (context: unstable_RouterContextProvider) => T ]; }
Using the Middleware
Now, you can instantiate the middleware with your class and arguments:
import { createSingletonMiddleware } from "./singleton-middleware"; import { Batcher } from "@edgefirst-dev/batcher"; let [batcherMiddleware, getBatcher] = createSingletonMiddleware({ Class: Batcher, arguments: [], });
Adding the Middleware to the Router
Next, add the middleware to a route of your choice:
import { batcherMiddleware } from "~/middleware/batcher"; export const unstable_middleware = [batcherMiddleware];
Accessing the Singleton Instance
Now, you can retrieve the singleton instance inside your route loaders:
import { getBatcher } from "~/middleware/batcher"; // routes/a.tsx export async function loader({ context }: Route.LoaderArgs) { let batcher = getBatcher(context); let viewer = await batcher.call(["viewer"], () => api.fetchViewer()); // ... } // routes/b.tsx export async function loader({ context }: Route.LoaderArgs) { let batcher = getBatcher(context); let viewer = await batcher.call(["viewer"], () => api.fetchViewer()); // ... }
Both route loaders will now share the same Batcher
instance within a single request.
When to Use This Pattern
This approach ensures a new instance of a class is created only when needed, making it ideal for per-request caching or request-specific batching. However, if you need to share the same instance across multiple requests, a traditional singleton pattern may be more appropriate.