The eslint-plugin-react-hooks documentation.
It is impossible to statically analyze React Context consumption by a component or a hook in React. This makes those prone to runtime errors when the context is not provided.
Enforce the context providers and consumers (components and hooks) to list the providing and consuming contexts in none-code autofixable JSDoc comments.
Everything starts from defining the @context tag in JSDoc comments for React Context entities:
/**
* @context ExampleContext
*/
const ExampleContext = createContext<{
value: number;
}>(null!);
/*
* π Use the non-null assertion operator to "guarantee" the context is required (does not have default value).
* The non-null assertion operator triggers the ESLint rule to enforce the presence of the `@context` in JSDoc.
* The `@context` tag refers to the context variable name, unless `displayName` is set.
*/
/**
* @context CustomName
*/
const NamedContext = createContext<{
value: string;
}>(undefined!); // using undefined! to indicate required context
NamedContext.displayName = "CustomName";
/*
* π The context name used in the `@context` tag can be customized by setting the `displayName` property on the context object.
*/
const OptionalContext = createContext<
| {
value: boolean;
}
| undefined
>(undefined);
/*
* π No `@context` tag is required for optional contexts (no non-null assertion operator used).
* The tag can set manually if desired.
*/
const DefaultContext = createContext<{
value: string;
}>({
value: "default",
});
/*
* π No `@context` tag is required for contexts with default values
* The tag can set manually if desired.
*/When the context is consumed by a component or a hook via useContext or use, the consuming entity must list the consumed contexts in the @consumes tag.
When a component or a hook with @consumes tag used in other components or hooks, those must also list all the required contexts transitively.
ESLint enforces that all required contexts are listed in the @consumes tag.
/**
* @consumes ExampleContext, CustomName
*/
function useAllExample() {
const requiredValue1 = useContext(ExampleContext);
const requiredValue2 = use(NamedContext);
const optionalValue = useContext(OptionalContext);
const defaultValue = useContext(DefaultContext);
return { requiredValue1, requiredValue2, optionalValue, defaultValue };
}
/**
* @consumes ExampleContext
*/
function useRequiredExample() {
return useContext(ExampleContext).value;
}
/**
* @consumes ExampleContext, CustomName
*/
function ExampleComponent() {
const required = useRequiredExample(); // transitively requires ExampleContext
const { value } = use(NamedContext); // explicitly consumes CustomName
return (
<div>
{required} + {value}
</div>
);
}
/**
* @consumes ExampleContext, CustomName
*/
function ParentComponent() {
return (
<div>
<ExampleComponent /> {/* transitively requires ExampleContext and CustomName */}
<ExampleComponent /> {/* transitively requires ExampleContext and CustomName */}
</div>
);
}When the context is provided by a component, the providing entity must list the provided contexts in the @provides tag.
When a component with @provides tag is used in other components, those must also list all the provided contexts transitively.
ESLint enforces that all provided contexts are listed in the @provides tag.
/**
* @provides ExampleContext
*/
function ExampleProvider({ children }) {
return <ExampleContext.Provider value={{ value: 42 }}>{children}</ExampleContext.Provider>;
}
/**
* @provides ExampleContext, CustomName
*/
function Providers({ children }) {
return (
<NamedContext.Provider value={{ value: "hello" }}>
{/* transitively provides ExampleContext */}
<ExampleProvider>{children}</ExampleProvider>
</NamedContext.Provider>
);
}When a component with @provides tag hosts components or hooks with @consumes tag, ESLint resolves the pairs and ensures the unresolved @provides and @consumes tags are mentioned:
/**
* @consumes CustomName
*/
function PartialApp() {
return (
<ExampleProvider>
{/* It consumes more than ExampleProvider provides, so CustomName transitively required */}
<ParentComponent />
</ExampleProvider>
);
}
function App() {
// The App component does not have @consumes nor @provides tag because Providers fulfills all required contexts.
return (
<NamedContext.Provider value={{ value: "hello" }}>
<PartialApp />
</NamedContext.Provider>
);
}Finally, the top-level rendering function must ensure that the root component does not have the @consumes tag, meaning all required contexts are provided.
const root = createRoot(document.getElementById("root")!);
// the .render call only render a component without `@consumes` tag
root.render(<App />);