# How to Use React Context with useState in TypeScript: Best Practices for Global State Management

> Master React Context and useState in TypeScript for global state. Discover best practices like generic context factories, memoization, and scoped providers to optimize performance and avoid re-renders.

- Repository: [Meta/react](https://github.com/facebook/react)
- Tags: best-practices
- Published: 2026-02-16

---

**Use a generic context factory to separate state and dispatch contexts, memoize provider values with `useMemo`, and scope providers to specific component trees to prevent unnecessary re-renders when managing global state with React Context and TypeScript.**

Managing global state in React applications often leads to prop drilling or overly complex external libraries. Using React Context with useState in TypeScript provides a lightweight, type-safe alternative for shared state, as demonstrated in the `facebook/react` repository's compiler playground implementation.

## Choose the Right State Management Abstraction

Selecting the appropriate API depends on your state complexity and update frequency.

### Simple Read-Only Values

For static data like themes or configuration objects, use `React.createContext<T>` with a default value. This requires no dispatcher since consumers only read the value.

### Mutable Global State with Type Safety

For mutable global state—such as authentication status or UI settings—implement a custom wrapper that returns separate `Provider`, `useContext`, and `useDispatch` hooks. The `facebook/react` compiler playground uses this pattern in [`compiler/apps/playground/lib/createContext.ts`](https://github.com/facebook/react/blob/main/compiler/apps/playground/lib/createContext.ts) to mirror the Redux pattern without external dependencies while maintaining full TypeScript type safety.

### Complex State Logic

When state transitions involve multiple actions or depend on previous state, replace `useState` with `useReducer`. This provides a stable dispatch function reference and makes state transitions explicit, as implemented in [`compiler/apps/playground/components/StoreContext.tsx`](https://github.com/facebook/react/blob/main/compiler/apps/playground/components/StoreContext.tsx).

## Stabilize Context Values to Prevent Re-renders

React re-renders all consumers whenever a `<Context.Provider>`'s `value` prop changes by reference. To optimize performance, memoize the context value.

In [`compiler/apps/playground/components/StoreContext.tsx`](https://github.com/facebook/react/blob/main/compiler/apps/playground/components/StoreContext.tsx), the provider uses `useMemo` to ensure the context value only changes when state or dispatch actually updates:

```typescript
import React, {useReducer, useMemo, ReactNode} from 'react';

type Store = {
  isPageReady: boolean;
};

type Action = {type: 'SET_READY'; payload: boolean};

const reducer = (state: Store, action: Action): Store => {
  switch (action.type) {
    case 'SET_READY':
      return {...state, isPageReady: action.payload};
    default:
      return state;
  }
};

const StoreContext = React.createContext<Store | undefined>(undefined);
const StoreDispatchContext = React.createContext<React.Dispatch<Action> | undefined>(undefined);

export const StoreProvider = ({children}: {children: ReactNode}) => {
  const [state, dispatch] = useReducer(reducer, {isPageReady: false});
  const memoState = useMemo(() => state, [state]);
  
  return (
    <StoreContext.Provider value={memoState}>
      <StoreDispatchContext.Provider value={dispatch}>
        {children}
      </StoreDispatchContext.Provider>
    </StoreContext.Provider>
  );
};

```

This pattern ensures that components such as `Header` only re-render when the specific slice of state they consume changes.

## Separate State and Dispatch Contexts

To prevent components that only dispatch actions from re-rendering when state updates, split your context into two separate contexts: one for state and one for dispatch.

The `createContext` factory in [`compiler/apps/playground/lib/createContext.ts`](https://github.com/facebook/react/blob/main/compiler/apps/playground/lib/createContext.ts) implements this separation:

```typescript
import * as React from 'react';

export default function createContext<T>() {
  const StateContext = React.createContext<T | undefined>(undefined);
  const DispatchContext = React.createContext<React.Dispatch<any> | undefined>(undefined);

  const Provider = ({
    children,
    value,
  }: {children: React.ReactNode; value: [T, React.Dispatch<any>]}) => {
    const [state, dispatch] = value;
    return (
      <StateContext.Provider value={state}>
        <DispatchContext.Provider value={dispatch}>
          {children}
        </DispatchContext.Provider>
      </StateContext.Provider>
    );
  };

  const useContext = () => {
    const ctx = React.useContext(StateContext);
    if (ctx === undefined) {
      throw new Error('useContext must be used within a Provider');
    }
    return ctx;
  };

  const useDispatch = () => {
    const ctx = React.useContext(DispatchContext);
    if (ctx === undefined) {
      throw new Error('useDispatch must be used within a Provider');
    }
    return ctx;
  };

  return {Provider, useContext, useDispatch};
}

```

This architecture allows `Header` components to subscribe only to state changes while action-dispatching buttons remain stable across renders.

## Implement Type-Safe Generic Contexts

TypeScript generics ensure that your context consumers receive correctly typed data without casting. Define your context using a generic factory function that captures the store shape:

```typescript
type Store = {
  user: User | null;
  theme: 'light' | 'dark';
};

const StoreContext = createContext<Store>();

```

When consuming the context in [`compiler/apps/playground/components/Header.tsx`](https://github.com/facebook/react/blob/main/compiler/apps/playground/components/Header.tsx), the hooks return fully typed values:

```typescript
import React from 'react';
import {useStore, useStoreDispatch} from './StoreContext';

export const Header = () => {
  const {isPageReady} = useStore();
  const dispatch = useStoreDispatch();

  const toggleReady = () => {
    dispatch({type: 'SET_READY', payload: !isPageReady});
  };

  return (
    <header>
      <h1>Playground</h1>
      <button onClick={toggleReady}>
        {isPageReady ? 'Reset' : 'Start'}
      </button>
    </header>
  );
};

```

The TypeScript compiler validates that `isPageReady` exists on the store and that dispatched actions match the `Action` type definition.

## Scope Context to Component Trees

Not all state requires global exposure. If a state slice is only relevant to a specific subtree—such as editor configuration or form state—colocate the provider with that subtree rather than placing it at the application root.

In [`compiler/apps/playground/components/Editor/ConfigEditor.tsx`](https://github.com/facebook/react/blob/main/compiler/apps/playground/components/Editor/ConfigEditor.tsx), local UI state is managed with `useState` while global store interactions use the context provider:

```typescript
import React, {useState} from 'react';
import {useStore} from '../StoreContext';

export const ConfigEditor = () => {
  // Local UI state - does not trigger global re-renders
  const [isExpanded, setIsExpanded] = useState<boolean>(false);
  
  // Global state from context
  const {isPageReady} = useStore();

  return (
    <div>
      <button onClick={() => setIsExpanded(!isExpanded)}>
        {isExpanded ? 'Collapse' : 'Expand'} Settings
      </button>
      {isExpanded && <div>Configuration options here...</div>}
      <p>Page ready: {isPageReady ? 'Yes' : 'No'}</p>
    </div>
  );
};

```

This approach keeps the global context lean and minimizes the number of components that re-render when global state changes.

## Summary

- **Use a generic context factory** like the one in [`compiler/apps/playground/lib/createContext.ts`](https://github.com/facebook/react/blob/main/compiler/apps/playground/lib/createContext.ts) to generate type-safe `Provider`, `useContext`, and `useDispatch` hooks.
- **Memoize context values** with `useMemo` in your provider to prevent unnecessary consumer re-renders when the provider component updates.
- **Split state and dispatch** into separate contexts to allow components that only dispatch actions to remain stable across state updates.
- **Leverage TypeScript generics** to ensure compile-time type safety for your store shape and action types.
- **Scope providers to subtrees** rather than always using global providers, and prefer `useState` for local UI concerns that do not require global access.

## Frequently Asked Questions

### Should I use useState or useReducer with React Context in TypeScript?

Use **useState** for simple primitive values or objects that update together, such as theme strings or boolean flags. Use **useReducer** when state transitions involve complex logic, multiple actions, or when you need a stable dispatch reference to prevent child components from re-rendering, as implemented in [`compiler/apps/playground/components/StoreContext.tsx`](https://github.com/facebook/react/blob/main/compiler/apps/playground/components/StoreContext.tsx).

### How do I prevent unnecessary re-renders when using React Context with useState?

Wrap your context value in **useMemo** to maintain referential equality between renders unless the underlying state actually changes. Additionally, split your context into separate state and dispatch contexts so that components subscribing only to the dispatcher do not re-render when state updates, following the pattern in [`compiler/apps/playground/lib/createContext.ts`](https://github.com/facebook/react/blob/main/compiler/apps/playground/lib/createContext.ts).

### Can I use React Context instead of Redux for global state management in TypeScript?

Yes, React Context combined with `useReducer` and a generic context factory provides a type-safe global state solution without external dependencies. The `facebook/react` compiler playground demonstrates this architecture by exporting typed `useStore` and `useStoreDispatch` hooks that offer Redux-like patterns while maintaining compile-time type safety through TypeScript generics.

### How do I make React Context type-safe with TypeScript?

Implement a **generic context factory** function that accepts a type parameter `<T>` and returns typed `Provider`, `useContext`, and `useDispatch` hooks. This pattern, found in [`compiler/apps/playground/lib/createContext.ts`](https://github.com/facebook/react/blob/main/compiler/apps/playground/lib/createContext.ts), ensures that consumers receive correctly typed state and dispatch functions without manual type casting or `any` annotations.