How to Share Data Between Components Using the Context API

Intermediate12 min interview
Skills tested:
Creating a typed context with createContext and a null default valueBuilding a Provider component that holds state and exposes actionsWriting a custom hook that reads the context and throws if used outside the providerUsing useMemo to stabilize the context value object and prevent unnecessary consumer re-rendersUnderstanding that all context consumers re-render when the context value changes

Advertisement

🧩 Scenario

Context API is the go-to solution for sharing state like the current user session, theme, locale, or any data accessed by many components at different tree depths. The pattern of a Provider plus a custom hook is the production-quality implementation: it keeps consumption ergonomic, enforces proper usage boundaries, and makes the context easy to mock in tests.

Architecture Walkthrough

createContext and the Null Default

createContext accepts a default value used only when a consumer component has no Provider above it in the tree. For contexts that always require a Provider (authentication, theme), passing null as the default and narrowing the type forces consumers to acknowledge the possibility of being outside a provider. A custom hook can catch this case and throw an error with a helpful message.

Using an empty object {} as the default silently returns an empty context to components outside a provider, which leads to subtle runtime errors. null makes the error explicit.

Provider Pattern

The Provider component wraps a subtree and supplies a value. Best practice is to co-locate the state management with the Provider so the context file is self-contained. The Provider uses useState or useReducer to manage the data, exposes both the data and the actions (setters, dispatch) through the context value, and wraps the value in useMemo to prevent a new object reference on every Provider render.

Custom Hook for Safe Consumption

Wrapping useContext in a custom hook named useSomething serves three purposes. It guards against usage outside the Provider with a clear error. It gives consumers a clean API without needing to import both useContext and the context object. And it makes the consuming code easier to test by mocking the hook rather than wrapping every test in a Provider.


Key Code Explained

// auth-context.tsx
interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthContextValue {
  user: User | null;
  isAuthenticated: boolean;
  signIn: (email: string, password: string) => Promise<void>;
  signOut: () => void;
}

// null default: forces usage inside AuthProvider
const AuthContext = createContext<AuthContextValue | null>(null);

// Custom hook: guards against usage outside provider
export function useAuth(): AuthContextValue {
  const ctx = useContext(AuthContext);
  if (!ctx) {
    throw new Error('useAuth must be called within an AuthProvider');
  }
  return ctx;
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const signIn = useCallback(async (email: string, password: string) => {
    const userData = await loginApi(email, password);
    setUser(userData);
  }, []);

  const signOut = useCallback(() => {
    setUser(null);
    clearSession();
  }, []);

  // useMemo: prevents new object reference on every AuthProvider render
  const value = useMemo<AuthContextValue>(
    () => ({
      user,
      isAuthenticated: user !== null,
      signIn,
      signOut,
    }),
    [user, signIn, signOut],
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}


// App setup
function App() {
  return (
    <AuthProvider>
      <Router>
        <AppRoutes />
      </Router>
    </AuthProvider>
  );
}


// Consumer: clean ergonomics, no context import needed
function NavBar() {
  const { user, isAuthenticated, signOut } = useAuth();

  return (
    <nav>
      {isAuthenticated ? (
        <>
          <span>{user!.name}</span>
          <button onClick={signOut}>Sign Out</button>
        </>
      ) : (
        <a href="/login">Sign In</a>
      )}
    </nav>
  );
}


// Splitting contexts for performance
// If theme changes rarely and user session changes separately:
function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        {children}
      </ThemeProvider>
    </AuthProvider>
  );
}
// Components reading theme do not re-render when user session changes
// Components reading auth do not re-render when theme changes

The isAuthenticated: user !== null field in the context value is an example of derived state that belongs in the context value object. Computing it once in the Provider means consumers do not need to repeat user !== null everywhere. Because isAuthenticated is derived from user, it updates automatically when user changes, with no extra state or effect needed.


Tradeoffs

Context value patternRe-render behaviorTypeScript safety
Object without useMemoRe-renders all consumers on every renderDepends on type
useMemo on valueOnly re-renders when deps changeSame
Split contextsEach context re-renders only its consumersRequires more files

What Interviewers Actually Check

  • Whether you know to pass null as the default and check for it in a custom hook
  • Whether you wrap the context value in useMemo to prevent unnecessary re-renders
  • Whether you write a custom hook rather than calling useContext directly in consumers
  • Whether you know that all consumers re-render on every context value change
  • Whether you can explain when splitting contexts is the right mitigation

Follow-Up Questions

  1. How would you provide a mock AuthProvider in tests that returns a specific user without network requests?
  2. How does React.lazy and Suspense interact with context: does a lazily loaded component that calls useAuth still have access to the Provider's value?
  3. How would you implement a multi-tenant context that provides different values based on a URL path segment?
  4. What is the useContextSelector library and how does it solve the per-field re-render granularity that built-in useContext lacks?
  5. How would you combine useReducer with Context to implement a Redux-like dispatch pattern without Redux?

Common Candidate Mistakes

  • Calling useContext(AuthContext) directly in components without a guard, getting null and a confusing error when used outside the provider
  • Not wrapping the context value in useMemo, causing all consumers to re-render on every parent re-render even when data is unchanged
  • Using a single context for all global state, causing all consumers to re-render when any part of the state changes
  • Using an empty object {} as the createContext default, silently returning nothing to components outside the provider
  • Not providing a TypeScript type for the context value, losing autocomplete and type safety in consumers

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you create a typed context with a null default value and a type union?
  • Can you build a Provider that manages state and exposes typed actions?
  • Can you write a custom hook that reads context and throws a descriptive error if called outside the provider?
  • Can you explain why useMemo on the context value prevents unnecessary consumer re-renders?
  • Can you describe the re-render behavior and how splitting contexts mitigates it?

Summary

The Context API shares values across a component tree without prop drilling. The production-quality implementation has three parts: a context created with createContext and a null default, a Provider component that manages state and wraps the value in useMemo, and a custom hook that reads the context and throws a helpful error if called outside the Provider.

useMemo on the context value object is essential because context comparison uses reference equality. Without it, the Provider creates a new object on every render, causing all consumers to re-render even when the data has not changed. With useMemo, consumers only re-render when the actual data in the value changes.

All consumers re-render when any field in the context value changes. For contexts that hold multiple pieces of state with different update frequencies, splitting into separate contexts prevents cross-concern re-renders. A ThemeContext that changes rarely does not cause NavBar re-renders when a CartContext that updates frequently receives a new item.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

When should I use Context instead of props?

When data needs to be accessed by multiple components at different depths and prop drilling would require passing through many intermediate components that do not use the value themselves.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement