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.