eMoosavi
Weekly frontend dev braindumps
Mastering React Context for Scalable State Management
State Management

Mastering React Context for Scalable State Management

Unlock the Hidden Powers of React Context

Jun 26, 2024 - 10:506 min read

Introduction

State management can quickly become complex in large-scale React applications. While libraries like Redux are popular, React's built-in Context API offers a powerful yet often underutilized alternative. This post delves into advanced techniques for using React Context, offering nuanced insights and practical code examples.

Why Consider React Context?

React Context provides a way to share state globally without prop drilling. While it’s often considered for smaller applications, it can be an efficient solution for more extensive systems if used correctly.

Understanding the Basics

Before diving into the advanced stuff, let’s revisit the basics:

import React, { createContext, useContext, useState } from 'react';

const MyContext = createContext();

const MyProvider = ({ children }) => {
  const [state, setState] = useState('default value');

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

const MyComponent = () => {
  const { state, setState } = useContext(MyContext);

  return (
    <div>
      <p>{state}</p>
      <button onClick={() => setState('new value')}>Change Value</button>
    </div>
  );
};

export default MyProvider;

Limitations of Basic Context Usage

  1. Re-rendering Issues: Every consumer re-renders when the context value changes, which can be costly.
  2. Scalability Problems: Managing multiple contexts for different states can become cumbersome.

Advanced Techniques

1. Separate State and Dispatch

One way to mitigate some of the issues is by separating state and dispatch into different contexts.

import React, { createContext, useContext, useReducer } from 'react';

const StateContext = createContext();
const DispatchContext = createContext();

const reducer = (state, action) => {
  switch (action.type) {
    case 'SET_VALUE':
      return { ...state, value: action.payload };
    default:
      return state;
  }
};

const MyProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, { value: 'default' });

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

const useGlobalState = () => useContext(StateContext);
const useGlobalDispatch = () => useContext(DispatchContext);

export { MyProvider, useGlobalState, useGlobalDispatch };

2. Memoizing Context Values

To prevent unnecessary re-renders, you can memoize context values using useMemo.

import React, { createContext, useContext, useReducer, useMemo } from 'react';

const StateContext = createContext();
const DispatchContext = createContext();

const reducer = (state, action) => {
  switch (action.type) {
    case 'SET_VALUE':
      return { ...state, value: action.payload };
    default:
      return state;
  }
};

const MyProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, { value: 'default' });

  const memoizedState = useMemo(() => ({ value: state.value }), [state.value]);

  return (
    <StateContext.Provider value={memoizedState}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
};

const useGlobalState = () => useContext(StateContext);
const useGlobalDispatch = () => useContext(DispatchContext);

export { MyProvider, useGlobalState, useGlobalDispatch };

3. Selective Context Updates

Another strategy involves selectively updating parts of the context. You can create multiple contexts managing different parts of the state.

import React, { createContext, useContext, useReducer, useMemo } from 'react';

const ValueContext = createContext();
const DispatchContext = createContext();

const reducer = (state, action) => {
  switch (action.type) {
    case 'SET_VALUE':
      return { ...state, value: action.payload };
    default:
      return state;
  }
};

const MyProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, { value: 'default' });

  const value = useMemo(() => ({ value: state.value }), [state.value]);

  return (
    <ValueContext.Provider value={value}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </ValueContext.Provider>
  );
};

const useValueContext = () => useContext(ValueContext);
const useDispatchContext = () => useContext(DispatchContext);

export { MyProvider, useValueContext, useDispatchContext };

4. Context Modules

A pattern to handle complex state management involves dividing your context into smaller, manageable modules.

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

const ValueContext = createContext();
export const useValueContext = () => useContext(ValueContext);
export default ValueContext;

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

const DispatchContext = createContext();
export const useDispatchContext = () => useContext(DispatchContext);
export default DispatchContext;

// MyProvider.js
import React, { useReducer, useMemo } from 'react';
import ValueContext from './valueContext';
import DispatchContext from './dispatchContext';

const reducer = (state, action) => {
  switch (action.type) {
    case 'SET_VALUE':
      return { ...state, value: action.payload };
  }
};

const MyProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, { value: 'default' });

  const value = useMemo(() => ({ value: state.value }), [state.value]);

  return (
    <ValueContext.Provider value={value}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </ValueContext.Provider>
  );
};

export default MyProvider;

5. Using Custom Hooks

Custom hooks can simplify consuming context in your components.

// useValue.js
import { useContext } from 'react';
import ValueContext from './valueContext';

const useValue = () => useContext(ValueContext);
export default useValue;

// useDispatch.js
import { useContext } from 'react';
import DispatchContext from './dispatchContext';

const useDispatch = () => useContext(DispatchContext);
export default useDispatch;

// MyComponent.js
import React from 'react';
import useValue from './useValue';
import useDispatch from './useDispatch';

const MyComponent = () => {
  const { value } = useValue();
  const dispatch = useDispatch();

  return (
    <div>
      <p>{value}</p>
      <button onClick={() => dispatch({ type: 'SET_VALUE', payload: 'new value' })}>Change Value</button>
    </div>
  );
};

export default MyComponent;

Performance Considerations

Using React Context efficiently requires attention to re-renders. Always ensure you:

  • Use memoization (useMemo, useCallback).
  • Split contexts when necessary.
  • Leverage custom hooks for modularity.

Case Study: Real World Application

To see these principles in action, let’s consider a sample application for managing a to-do list with React Context.

Step 1: Define Contexts

// toDoContext.js
import { createContext } from 'react';

const ToDoContext = createContext();
export default ToDoContext;

// toDoDispatchContext.js
import { createContext } from 'react';

const ToDoDispatchContext = createContext();
export default ToDoDispatchContext;

Step 2: Create Provider

// ToDoProvider.js
import React, { useReducer, useMemo } from 'react';
import ToDoContext from './toDoContext';
import ToDoDispatchContext from './toDoDispatchContext';

const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, action.payload];
    case 'REMOVE_TODO':
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
};

const ToDoProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, []);

  const memoizedState = useMemo(() => state, [state]);

  return (
    <ToDoContext.Provider value={memoizedState}>
      <ToDoDispatchContext.Provider value={dispatch}>
        {children}
      </ToDoDispatchContext.Provider>
    </ToDoContext.Provider>
  );
};

export default ToDoProvider;

Step 3: Use Context in Components

// ToDoList.js
import React from 'react';
import { useContext } from 'react';
import ToDoContext from './toDoContext';

const ToDoList = () => {
  const toDos = useContext(ToDoContext);

  return (
    <ul>
      {toDos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
};

export default ToDoList;

// AddToDo.js
import React, { useState } from 'react';
import { useContext } from 'react';
import ToDoDispatchContext from './toDoDispatchContext';

const AddToDo = () => {
  const [text, setText] = useState('');
  const dispatch = useContext(ToDoDispatchContext);

  return (
    <div>
      <input
        type='text'
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => dispatch({ type: 'ADD_TODO', payload: { id: Date.now(), text } })}>
        Add
      </button>
    </div>
  );
};

export default AddToDo;

Conclusion

React Context is a powerful tool for state management when used judiciously. By incorporating advanced techniques like separating state and dispatch, memoizing context values, and leveraging custom hooks, you can create scalable and maintainable architectures. Remember to monitor performance and adjust strategies as your application grows.

Happy coding!

Article tags
reactcontextstate-managementadvanced-techniquesstate
Previous article

Advanced JavaScript

Harnessing TypeScript's Power with Advanced Utility Types in React

Next article

Frontend Architecture

Deep Dive into React Suspense and Concurrent Features