Context API

Introduction

React's current context API is released in React version 16.3 and it replaced the legacy context API which had some design flaws. The major flaw of legacy API was that if a context value provided by component changes, descendants that use that value won’t update if an intermediate parent returns false from shouldComponentUpdate. createContext() overcome this flaw. So, even if the component in the middle skips rendering, an update to a value will be seen in child components. But what exact problems does context API solve? According to the React documentation.

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

Context essentially solves the problem of prop drilling. So is prop drilling that bad?. The answer is not always, prop drilling becomes an issue when the desired component is nested deeply in the component tree. So when should you use context API?

When to use Context API?

According to react documentation

Context is primarily used when some data needs to be accessible by many components at different nesting levels. Apply it sparingly because it makes component reuse more difficult.

So definitely don't put everything into the context. Sebastian Markbåge(React core team architect) suggested that use context for values that are hardly changing for example theme/locale. Context API is not a state management solution it does not manage your application's state by definition context is a form of dependency injection. useContext() doesn't let you subscribe to a part of the context value (or some memoized selector) without fully re-rendering. So you can face some performance issues by overusing context. You can think of moving to some state management library like Redux or Mobx for effective state management.

useContext + useReducer

This is a very common pattern you might have observed in a lot of places. useReducer is mainly preferred if you have complex state operations to perform. Otherwise, it's overkill to use the useReducer() and you can also stick to useState() if you have simple state logic. Let's try a simple counter example with context.

  • The first thing we can do is create a context object using createContext()
import { createContext } from "react";

const CountContext = createContext();
  • You can create your own provider component like this and then wrap around whichever should consume this value.
const CountProvider = ({ children }) => {
  const [countState, countDispatch] = useReducer(countReducer, initialState);
  return (
    <CountContext.Provider value={{ countState, countDispatch }}>
      {children}
    </CountContext.Provider>
  );
};
  • Rather than consuming the state directly you can make a custom hook like this.
const useCount = () => {
  const context = useContext(CountContext);
  if (context === undefined) {
    throw new Error("useCount must be used within a CountProvider");
  }
  return context;
};
  • Now our reducer and initial state.
const initialState = {
  count: 0
};
const countReducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT_BY_10":
      return { ...state, count: state.count + 10 };
    case "DECREMENT_BY_10":
      return { ...state, count: state.count - 10 };
    case "INCREMENT_BY":
      return { ...state, count: state.count + action.payload };
    default:
      return state;
  }
};

export { initialState, countReducer };
  • How to dispatch?
const Counter = () =>{
 const {
 countState: { count, isAdding },
 countDispatch
  } = useCount();

return(
<>
<h1>{count}</h1>

<button onClick={() => countDispatch({ type: "INCREMENT_BY_10" })}>
    +10
</button>

<button onClick={() => countDispatch({ type: "DECREMENT_BY_10" })}> 
    -10
</button>
</>
)}
  • Now, what about asynchronous actions? This is our async incrementer function
const incrementAsyncBy = (amount) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(amount);
    }, 500);
  });
};
  • We can make the following changes in our reducer
const initialState = {
  count: 0,
  isAdding: false
};
const countReducer = (state, action) => {
     {...}
    case "START_ADDING":
      return { ...state, isAdding: true };
    case "DONE_ADDING":
      return { ...state, isAdding: false };
    default:
      return state;
  }
};
  • Handle asynchronous operations like this
const handleAsynIncrement = async () => {
    countDispatch({ type: "START_ADDING" });
    const amount = await incrementAsyncBy(10);
    countDispatch({ type: "INCREMENT_BY", payload: Number(amount) });
    countDispatch({ type: "DONE_ADDING" });
  };

References