DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 15: React Context API

React Context API

Introduction

The React Context API is a powerful feature that enables efficient state management and data sharing across components without having to pass props manually through every level of the component tree. Context provides a way to share values between components without explicitly passing a prop through every level of the tree. This is particularly useful for sharing data that can be considered “global” for a tree of React components.

Understanding the Problem Context Solves

Before diving into how Context works, let’s understand the problem it aims to solve: prop drilling.

Prop Drilling

Prop drilling occurs when you need to pass data from a component at the top of your component tree to a deeply nested component. This often means passing props through intermediate components that don’t need the data themselves but merely serve as conduits.

Consider the following component structure:

App
└── Header
    └── Navigation
        └── UserMenu
            └── UserAvatar

If you have user information at the App level that the UserAvatar component needs, you would traditionally pass it as props:

// App.js
function App() {
  const user = { name: "John Doe", avatar: "johndoe.jpg" };
  return <Header user={user} />;
}

// Header.js
function Header({ user }) {
  return <Navigation user={user} />;
}

// Navigation.js
function Navigation({ user }) {
  return <UserMenu user={user} />;
}

// UserMenu.js
function UserMenu({ user }) {
  return <UserAvatar user={user} />;
}

// UserAvatar.js
function UserAvatar({ user }) {
  return <img src={user.avatar} alt={user.name} />;
}

As you can see, Header and Navigation don’t need the user information but must accept and pass it down to reach UserAvatar. This is prop drilling, and it can make your code harder to maintain, especially in large applications.

Core Concepts of Context API

The Context API consists of three main parts:

  1. React.createContext: Creates a Context object
  2. Context.Provider: A React component that provides the context value to consuming components
  3. Context.Consumer or useContext Hook: Ways to consume and access the context value

React.createContext

const MyContext = React.createContext(defaultValue);

createContext creates a Context object. When React renders a component that subscribes to this Context object, it will read the current context value from the closest matching Provider above it in the tree.

The defaultValue argument is only used when a component does not have a matching Provider above it in the tree. This default value can be helpful for testing components in isolation.

Context.Provider

<MyContext.Provider value={/* some value */}>
  {/* descendant components */}
</MyContext.Provider>

Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes.

The Provider component accepts a value prop to be passed to consuming components that are descendants of this Provider. One Provider can be connected to many consumers.

Context.Consumer and useContext Hook

There are two ways to consume context:

1. Using Context.Consumer (Class and Function Components)

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

2. Using useContext Hook (Function Components only)

const value = useContext(MyContext);

Creating and Using Context

Let’s implement a simple theme switching feature to demonstrate the Context API:

// ThemeContext.js
import React, { createContext, useState } from 'react';

// Create a context with a default value
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {},
});

// Create a provider component
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export default ThemeContext;

// App.js
import React from 'react';
import { ThemeProvider } from './ThemeContext';
import ThemedComponent from './ThemedComponent';

function App() {
  return (
    <ThemeProvider>
      <div className="App">
        <h1>Theme Context Example</h1>
        <ThemedComponent />
      </div>
    </ThemeProvider>
  );
}

export default App;

// ThemedComponent.js
import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';

function ThemedComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  const styles = {
    padding: '20px',
    backgroundColor: theme === 'light' ? '#fff' : '#333',
    color: theme === 'light' ? '#333' : '#fff',
    border: '1px solid #ddd',
    borderRadius: '4px',
  };

  return (
    <div style={styles}>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

export default ThemedComponent;

In this example:

  1. We create a ThemeContext with a default value containing the theme state and a toggle function.
  2. We define a ThemeProvider component that maintains the theme state and provides it to all child components.
  3. We wrap our App with the ThemeProvider.
  4. In the ThemedComponent, we consume the context using the useContext hook and use the theme state to conditionally style our component.

Context API vs. Prop Drilling

Context APIProp Drilling
Avoids passing props through intermediate componentsRequires passing props through each level of the component tree
Makes component reuse easierMakes component reuse more difficult
Simplifies component APICan lead to complex component APIs with many props
Can decrease performance if overusedCan be more performant for small component trees
Can make component testing more complexUsually easier to test components in isolation

Using Context with Hooks

The introduction of hooks in React 16.8 made using Context even easier. The useContext hook is a cleaner way to consume context compared to Context.Consumer.

import React, { useContext } from 'react';
import UserContext from './UserContext';

function ProfilePage() {
  const user = useContext(UserContext);

  return (
    <div>
      <h1>{user.name}'s Profile</h1>
      <img src={user.avatar} alt={user.name} />
      <p>Email: {user.email}</p>
    </div>
  );
}

Creating Multiple Contexts

For complex applications, you might need multiple contexts for different types of data:

// UserContext.js
export const UserContext = createContext(null);

// ThemeContext.js
export const ThemeContext = createContext('light');

// NotificationContext.js
export const NotificationContext = createContext([]);

You can then nest these providers:

function App() {
  const user = getUser();
  const theme = getTheme();
  const notifications = getNotifications();

  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <NotificationContext.Provider value={notifications}>
          <MainApp />
        </NotificationContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

Best Practices for Using Context

1. Don’t Overuse Context

While Context can help reduce prop drilling, it’s not meant to replace all component communication. Use Context for truly global data that many components need access to, such as:

  • Theme preferences
  • User authentication
  • Localization/language preferences
  • Feature flags

2. Split Contexts by Domain

Instead of creating one large context for your entire application, create separate contexts for different domains of your application. This improves maintainability and prevents unnecessary re-renders.

3. Memoize Context Values

To avoid unnecessary re-renders when a context value changes, use useMemo to memoize the value:

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = useCallback(() => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  }, []);

  const value = useMemo(() => {
    return { theme, toggleTheme };
  }, [theme, toggleTheme]);

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

4. Use Context Selectors

For large contexts, consider creating selector hooks that extract only the data needed by a specific component:

// useUserName.js
export function useUserName() {
  const { name } = useContext(UserContext);
  return name;
}

// useUserAvatar.js
export function useUserAvatar() {
  const { avatar } = useContext(UserContext);
  return avatar;
}

Then use these selector hooks in your components:

function UserGreeting() {
  const name = useUserName();
  return <h1>Hello, {name}!</h1>;
}

Common Patterns with Context

1. Context + Reducer Pattern

Combining Context with a reducer can create a powerful state management solution similar to Redux but built into React:

// UserContext.js
import { createContext, useReducer, useContext } from 'react';

// Create the context
const UserContext = createContext();

// Define the initial state
const initialState = {
  user: null,
  isLoading: false,
  error: null
};

// Define the reducer
function userReducer(state, action) {
  switch (action.type) {
    case 'LOGIN_REQUEST':
      return { ...state, isLoading: true };
    case 'LOGIN_SUCCESS':
      return { ...state, isLoading: false, user: action.payload };
    case 'LOGIN_FAILURE':
      return { ...state, isLoading: false, error: action.payload };
    case 'LOGOUT':
      return { ...state, user: null };
    default:
      return state;
  }
}

// Create a provider component
export function UserProvider({ children }) {
  const [state, dispatch] = useReducer(userReducer, initialState);

  return (
    <UserContext.Provider value={{ state, dispatch }}>
      {children}
    </UserContext.Provider>
  );
}

// Create a hook for using this context
export function useUser() {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

// LoginComponent.js
import { useUser } from './UserContext';

function LoginComponent() {
  const { state, dispatch } = useUser();
  const { user, isLoading, error } = state;

  const handleLogin = async (credentials) => {
    dispatch({ type: 'LOGIN_REQUEST' });
    try {
      const user = await loginAPI(credentials);
      dispatch({ type: 'LOGIN_SUCCESS', payload: user });
    } catch (error) {
      dispatch({ type: 'LOGIN_FAILURE', payload: error.message });
    }
  };

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (user) return <div>Welcome, {user.name}!</div>;

  return (
    <form onSubmit={handleLogin}>
      {/* Login form fields */}
    </form>
  );
}

This pattern is often referred to as the “useReducer + Context” pattern and provides a Redux-like state management solution using only React’s built-in features.

2. Composing Providers

For complex applications with multiple contexts, you can create a composition of providers:

// AppProviders.js
import { ThemeProvider } from './ThemeContext';
import { UserProvider } from './UserContext';
import { LocaleProvider } from './LocaleContext';

function AppProviders({ children }) {
  return (
    <ThemeProvider>
      <LocaleProvider>
        <UserProvider>
          {children}
        </UserProvider>
      </LocaleProvider>
    </ThemeProvider>
  );
}

export default AppProviders;

Then use it in your App:

// App.js
import AppProviders from './AppProviders';

function App() {
  return (
    <AppProviders>
      <MainApp />
    </AppProviders>
  );
}

Limitations and Considerations

While Context is powerful, it comes with some limitations and considerations:

1. Performance Implications

Any component that consumes a context will re-render when the context value changes. If you place a large data structure in context and frequently update it, it could lead to performance issues.

To mitigate this:

  • Split your context into smaller, more focused contexts
  • Use memoization with useMemo and useCallback
  • Implement selective context updates (only update what’s needed)

2. Not Suitable for High-Frequency Updates

Context is not optimized for high-frequency updates (like several times per second). If you need this, consider using component state or a specialized state management library like Redux.

3. Context API vs. State Management Libraries

The Context API is not a complete state management solution like Redux or MobX. It’s primarily designed for dependency injection and global state that doesn’t change frequently.

Practical Examples

Example 1: User Authentication with Context

// AuthContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [currentUser, setCurrentUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // Sample authentication logic
  useEffect(() => {
    // Check if user is logged in (e.g., from localStorage)
    const userFromStorage = localStorage.getItem('user');
    if (userFromStorage) {
      setCurrentUser(JSON.parse(userFromStorage));
    }
    setLoading(false);
  }, []);

  const login = (user) => {
    setCurrentUser(user);
    localStorage.setItem('user', JSON.stringify(user));
  };

  const logout = () => {
    setCurrentUser(null);
    localStorage.removeItem('user');
  };

  const value = {
    currentUser,
    loading,
    login,
    logout
  };

  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  return useContext(AuthContext);
}

// PrivateRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { useAuth } from './AuthContext';

function PrivateRoute({ component: Component, ...rest }) {
  const { currentUser } = useAuth();

  return (
    <Route
      {...rest}
      render={props => {
        return currentUser ? <Component {...props} /> : <Redirect to="/login" />;
      }}
    />
  );
}

export default PrivateRoute;

// LoginPage.js
import React, { useState } from 'react';
import { useAuth } from './AuthContext';
import { useHistory } from 'react-router-dom';

function LoginPage() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { login } = useAuth();
  const history = useHistory();

  const handleSubmit = (e) => {
    e.preventDefault();
    // In a real app, you would validate and call an API
    login({ email });
    history.push('/dashboard');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
      />
      <button type="submit">Login</button>
    </form>
  );
}

export default LoginPage;

Example 2: Multi-Language Support

// LocaleContext.js
import React, { createContext, useState, useContext } from 'react';

// Define translations
const translations = {
  en: {
    greeting: 'Hello',
    farewell: 'Goodbye',
    welcome: 'Welcome to our app',
  },
  es: {
    greeting: 'Hola',
    farewell: 'Adiós',
    welcome: 'Bienvenido a nuestra aplicación',
  },
  fr: {
    greeting: 'Bonjour',
    farewell: 'Au revoir',
    welcome: 'Bienvenue dans notre application',
  },
};

const LocaleContext = createContext();

export function LocaleProvider({ children }) {
  const [locale, setLocale] = useState('en');

  const changeLocale = (newLocale) => {
    if (translations[newLocale]) {
      setLocale(newLocale);
    }
  };

  const t = (key) => {
    return translations[locale][key] || key;
  };

  const value = {
    locale,
    changeLocale,
    t,
  };

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

export function useLocale() {
  return useContext(LocaleContext);
}

// LanguageSwitcher.js
import React from 'react';
import { useLocale } from './LocaleContext';

function LanguageSwitcher() {
  const { locale, changeLocale } = useLocale();

  return (
    <div>
      <button 
        onClick={() => changeLocale('en')}
        disabled={locale === 'en'}
      >
        English
      </button>
      <button 
        onClick={() => changeLocale('es')}
        disabled={locale === 'es'}
      >
        Español
      </button>
      <button 
        onClick={() => changeLocale('fr')}
        disabled={locale === 'fr'}
      >
        Français
      </button>
    </div>
  );
}

export default LanguageSwitcher;

// WelcomeMessage.js
import React from 'react';
import { useLocale } from './LocaleContext';

function WelcomeMessage() {
  const { t } = useLocale();

  return (
    <div>
      <h1>{t('greeting')}!</h1>
      <p>{t('welcome')}</p>
    </div>
  );
}

export default WelcomeMessage;

Summary

The React Context API provides a powerful way to share data across your component tree without the need for prop drilling. It’s particularly useful for global application state like themes, user authentication, and localization.

Key points to remember:

  1. Context is designed for sharing data that can be considered “global” for a tree of React components.
  2. It’s perfect for relatively static data that doesn’t change frequently.
  3. The Context API consists of three main parts: createContext, Context.Provider, and Context.Consumer or useContext.
  4. For complex state management, consider combining Context with useReducer.
  5. Be mindful of performance implications, especially with large data structures or frequent updates.
  6. Split your contexts by domain to improve maintainability.
  7. Use custom hooks to encapsulate context logic and make it easier to reuse.

When used appropriately, the Context API can significantly simplify your React applications by reducing prop drilling and making component reuse easier. However, for complex state management needs or high-frequency updates, you might want to consider other state management libraries, which we’ll cover in later chapters.

Exercises

  1. Create a simple theme switcher using the Context API that allows users to toggle between light and dark themes.
  2. Implement a shopping cart using Context and useReducer.
  3. Build a multi-language support system using Context and allow users to switch between at least two languages.
  4. Create a notification system using Context that allows different components to dispatch notifications.
  5. Refactor an existing application that uses prop drilling to use Context instead.

Further Reading

Scroll to Top