Context cheatsheet.

Basic

const ThemeContext = createContext<"light" | "dark">("light");

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Child />
    </ThemeContext.Provider>
  );
}

function Child() {
  const theme = useContext(ThemeContext);
  return <p>{theme}</p>;
}

React 19: shorthand provider

<ThemeContext value="dark">
  <Child />
</ThemeContext>

.Provider is now optional in React 19.

With null default + custom hook

const AuthContext = createContext<AuthState | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState<AuthState>({ user: null });
  const value = useMemo(() => ({ state, setState }), [state]);
  return <AuthContext value={value}>{children}</AuthContext>;
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth outside provider");
  return ctx;
}

Custom hook + null default = enforced provider boundary, friendly errors.

Multiple contexts (split)

const StateContext = createContext<State | null>(null);
const DispatchContext = createContext<Dispatch | null>(null);

function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, initial);
  return (
    <StateContext value={state}>
      <DispatchContext value={dispatch}>
        {children}
      </DispatchContext>
    </StateContext>
  );
}

Why split: components that only need dispatch don’t re-render when state changes.

Memoizing value

// BAD: new object every render → all consumers re-render
<ThemeContext value={{ theme: "dark" }}>

// GOOD
const value = useMemo(() => ({ theme }), [theme]);
<ThemeContext value={value}>

use(Context) in React 19

function Comp({ cond }: { cond: boolean }) {
  if (cond) {
    const theme = use(ThemeContext);   // OK conditionally
    return <p>{theme}</p>;
  }
  return null;
}

use can be called conditionally, unlike useContext.

When NOT to use context

  • Frequently-updating state shared widely → render storms. Use a store (Zustand, Jotai, Redux).
  • Passing data 1-2 levels deep → just use props.
  • “Avoiding prop drilling” — sometimes drilling is fine.

Combining providers

Nest readably:

function Providers({ children }) {
  return (
    <Theme>
      <Auth>
        <Router>
          {children}
        </Router>
      </Auth>
    </Theme>
  );
}

Or compose:

function compose(...providers: React.FC<{ children: React.ReactNode }>[]) {
  return ({ children }: { children: React.ReactNode }) =>
    providers.reduceRight((acc, P) => <P>{acc}</P>, <>{children}</>);
}

const Providers = compose(Theme, Auth, Router);

Provider with side-effect setup

function AuthProvider({ children }) {
  const [user, setUser] = useState<User | null>(null);
  
  useEffect(() => {
    return subscribeToAuth((u) => setUser(u));
  }, []);
  
  const value = useMemo(() => ({ user }), [user]);
  return <AuthContext value={value}>{children}</AuthContext>;
}

Selector pattern

Built-in context doesn’t support selectors. To get them, use:

  • use-context-selector: drop-in selector subscription
  • zustand: store with subscription-based selectors
// Zustand example
const userId = useStore((s) => s.user.id);   // re-renders only on id change

Server vs client context

In RSC: context cannot cross client/server boundary directly. Pass via props or use server-component-friendly patterns.

Common mistakes

  • Object literal as value — re-renders all consumers every render.
  • Using context for high-frequency state (mouse pos, animation).
  • Forgetting provider → consumer reads default value silently.
  • Single huge context with everything → un-debuggable re-renders.
  • Setting state inside reading component triggering chain re-renders.

Read this next

If you want my context + auth setup, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .