Benjamin Raffetseder
posts/Best Practices for Using React Context

Best Practices for Using React Context

React Context is a powerful feature that allows components to share state without prop drilling. It's particularly useful for managing global state, theme preferences, user authentication, and more. However, like any tool, it's important to use React Context effectively to ensure maintainable and organized code. In this post, we'll delve into best practices for using React Context, and why exporting your useContext logic to a custom hook is a wise decision.

Understanding React Context

Before we dive into best practices, let's quickly review what React Context is. React Context provides us a way to pass data through the component tree without having to manually pass props down the hierarchy. It consists of three main components:

  1. createContext: This function creates a new context object. It takes an initial value as an argument, which is used when a component doesn't have a matching provider.

  2. Context.Provider: This component is used to wrap the portion of the component tree where you want to make the context available. It accepts a value prop that holds the data you want to share.

  3. useContext: This hook is used to consume the context value in a functional component. It accepts a context object created using createContext and returns the current context value.

Best Practices for Using React Context

1. Use Context for Global State

React Context is best suited for managing global state that multiple components need to access. Avoid using it excessively for local state that only affects a single component.

2. Avoid Nesting Providers

Nesting multiple context providers can lead to performance issues as each provider creates a new context. If possible, consolidate related state into a single context to reduce nesting.

3. Export Context as a Separate Module

Create a dedicated file for your context and export the context object itself. This makes it easier to import and use the context in different parts of your application.

// ThemeContext.ts
import { createContext } from 'react';

export const ThemeContext = createContext<string>('light');

4. Export useContext Logic as a Custom Hook

Instead of using useContext directly in your components, encapsulate the logic within a custom hook. This makes your code more organized and reusable. To make sure that the hook is only used within a provider, you can throw an error if the context is undefined.

// useTheme.ts
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider')
  }
  return context
}

5. Type Safety with TypeScript

Use TypeScript to add type safety to your context and custom hook. Define the types for your context value and ensure your custom hook returns the correct type.

// ThemeContext.ts
export const ThemeContext = createContext<'light' | 'dark'>('light');

// useTheme.ts
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

export function useTheme(): 'light' | 'dark' {
  return useContext(ThemeContext);
}

6. Lazy Initialization

You can lazily initialize context values to avoid unnecessary computations until the context is actually used. This can be especially helpful for optimizing performance.

// LazyThemeContext.ts
import { createContext } from 'react';

export const LazyThemeContext = createContext<null | 'light' | 'dark'>(null);

// LazyThemeProvider.tsx
import React, { useState } from 'react';
import { LazyThemeContext } from './LazyThemeContext';

export function LazyThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  return (
    <LazyThemeContext.Provider value={theme}>
      {children}
    </LazyThemeContext.Provider>
  );
}

7. Use Memoization for Value

When providing a complex value through context, use memoization techniques (e.g., useMemo) to prevent unnecessary re-renders.

// ComplexContext.ts
import { createContext, useMemo } from 'react';

interface ComplexValue {
  // ...
}

export const ComplexContext = createContext<ComplexValue | undefined>(undefined);

// ComplexProvider.tsx
import React from 'react';
import { ComplexContext } from './ComplexContext';

export function ComplexProvider({ children }: { children: React.ReactNode }) {
  const complexValue = useMemo(() => {
    // Compute complex value here
  }, []);

  return (
      <ComplexContext.Provider value={complexValue}>
      {children}
    </ComplexContext.Provider>
  );
}

Conclusion

React Context is a powerful tool for managing global state and sharing data across components. By following best practices like exporting your useContext logic to custom hooks, using TypeScript for type safety, and optimizing for performance, you can create a maintainable and efficient application architecture.

These practices will not only make your codebase cleaner but also enhance the reusability and scalability of your React applications.