Definition
A generic context factory is a pattern where you create a React context inside a generic function, and the components created in that same function capture the context by closure.
One call gives you "a set of components that are completely separate — in type and in runtime object." The reason you need this is simple: a const declaration can't take type parameters.
Why you need it
Say several domains reuse a compound component that you assemble from pieces, like Table.Root / Table.Head / Table.Body. The orders table uses the order type, the users table uses the user type — the shape of a row differs per domain.
You want to carry that type through the context, but the syntax blocks you.
function f<T>(x: T) {} // ✅ functions can take type parameters
type Box<T> = { v: T }; // ✅ type aliases can too
const Ctx<T> = createContext<T>(null); // ❌ const declarations can't
createContext(...) is a value, not a type. The moment you call it, the type is pinned to one thing.
Two workarounds come to mind first, and both fail.
Create it as unknown and cast with as at every consumption point — you end up hardcoding the domain type inside the pieces. You built the component to be reusable, and now it's tied to a domain, so the point is gone.
Copy the file per domain — the entire table logic is duplicated. When you fix a bug, you fix it once per copy.
How it works
The fix is to create the context inside a generic function and let the components created alongside it capture it by closure.
export function createDataTable<Row>() {
type ContextValue = {
columns: readonly ColumnDef<Row>[];
rows: readonly Row[];
getRowId: (row: Row) => string;
};
const Ctx = createContext<ContextValue | null>(null);
const useTable = () => {
const c = useContext(Ctx);
if (!c) throw new Error("Table part must be used inside Table.Root");
return c;
};
function Root<const Cols extends readonly ColumnDef<Row>[]>(props: RootProps<Row, Cols>) {
return <Ctx.Provider value={/* ... */}>{props.children}</Ctx.Provider>;
}
function Body() {
const { columns, rows, getRowId } = useTable(); // ← Row is still alive, no `as` needed
return <tbody>{rows.map((r) => <Row key={getRowId(r)} data={r} />)}</tbody>;
}
return { Root, Head, Body }; // we don't export Ctx itself
}
Three things are happening here.
One. Ctx is created fresh on every call to createDataTable. So one call = one fully independent set. If you mix pieces from different factories, they can't find the context and throw at runtime.
Two. Body doesn't need to know what Row is. It captures Ctx by closure, and Ctx's type is already filled in with Row. The type survives without a single as.
Three. Narrowing with if (!c) throw pins the type to ContextValue without a ! non-null assertion. TypeScript knows c can't be null after the throw.
Why there are two layers of generics
You specify Row by hand when you call the factory, and Root infers the column type Cols automatically from the columns prop. The layers split because TypeScript has no syntax for specifying only some type arguments. If you put both in one signature, then the moment you specify Row it demands Cols too.
Applying it in practice
// orders/table.ts — exactly once, at module top level
export const OrderTable = createDataTable<OrderRow>();
// orders/page.tsx
<OrderTable.Root columns={cols} rows={orders} getRowId={(r) => r.orderNo}>
<OrderTable.Head />
<OrderTable.Body />
</OrderTable.Root>
Pass columns from another domain and it fails to compile; mix in a piece from another factory and it throws at runtime.
Two rules you must keep
Call the factory exactly once, at module top level.
// ❌ called inside a component body
function OrderPage() {
const Table = createDataTable<OrderRow>(); // a new set on every render
return <Table.Root ...>...</Table.Root>;
}
This doesn't error. The context wiring is fine too, because the provider and the consumer came out of the same closure.
The real problem is somewhere else. On every render Table.Root gets a new function identity, and React treats that as a different component, so it unmounts and remounts. The symptom isn't an error — it's "scroll position, input values, and focus keep disappearing" — which makes the cause far harder to find.
Don't put the Context itself in the returned object.
The only reason a single as isolated inside the factory is safe is that "Root only ever calls that callback with its own narrowed value." Export Ctx and anyone can put any value into the provider, and at that moment the assertion becomes a real bug.
Trade-offs
What you get is type-safe reuse. One set of logic gives you as many tables as you have domains, and none of them mix.
What you pay comes in three parts.
First, the function components live inside the factory, so their names can get fuzzy in React DevTools. It's worth setting displayName explicitly.
Second, bundle splitting gets harder. The pieces are bundled inside one function, so you can't lazy load just Body.
Third, the call-site rule isn't enforced by code. Calling at module top level is only a convention, so types won't stop a new teammate from calling it inside a component. You defend that with a lint rule or code review.
If you only have one domain, there's no reason to pay this cost. Plain createContext<OrderTableValue>(null) reads much better.
When not to use it
- When you have one or two domains. The factory costs more than it gives. Just create the context with a concrete type.
- When the pieces need to be lazy loaded individually. They're bundled inside one function, so code splitting doesn't work.
- In structures that cross the server component boundary. Context is client-only, so calling the factory in a server component won't work.
- When the type parameter never actually varies. If you opened it up with generics but every call site passes the same type, you've only added complexity.
Common mistakes
- Calling the factory inside a component body. No error, just endless remounting. It's the classic trap that has you chasing a state-loss bug for days.
- Putting
Ctxin the returned object. The basis for the isolation disappears, and the internal assertion becomes a real bug. - Wrapping a generic component in
memo.memoturns a generic component into a non-generic one, killing column type inference entirely. Use memo only on leaf components that have no generics. - Trying to use types to prevent using a piece outside
Root. To prevent it you end up with prop drilling, which erases the benefit of a compound component. Defend with a runtime throw plus tests, and write the remaining gap down in the docs — that's the right deal. - Not setting
displayName. Everything shows up as an anonymous function in DevTools, which makes debugging painful.
Related concepts
- derive-types-from-values — the principle behind
Rootderiving the sort key type from the column array - react-context-render-granularity — the re-render scope problem for the pieces subscribing to this context
- typescript-variance — how strictly the parameters of a callback carried in context are checked