Andreas Oikon
Andreas Oikon

Reputation: 65

How to handle multiple Contexts using React Context API?

I have a question about React's Context API. My coding level with React is beginner.

I am building an application that has 8 contexts and they may multiply in the future of the project. They are basic CRUD contexts for the different elements of my application without much complexity.

As I am writing my application I notice that a nested context hell is created in my App.js

To give more information I will explain a portion of the app. I have a Context for CRUD actions for Coaches, Athletes, Courts etc.

In my folder structure under /src directory I have a /context directory and inside I have a separate folder for each entity. Let's take Coaches as an example. In the /src/context/coach directory I have 3 files. A coachContext.js, a coachReducer.js and a CoachState.js

Contents of coachContext.js file:

import { createContext } from "react";

const coachContext = createContext();

export default coachContext;

Contents of coachReducer.js file:

const coachReducer = (state, action) => {
    switch (action.type) {
        case "GET_COACHES":
            return {
                ...state,
                coaches: action.payload,
            };
        case "SET_CURRENT_COACH":
            return {
                ...state,
                coach: action.payload,
                loading: false,
            };

        default:
            return state;
    }
};

export default coachReducer;

Contents of CoachState.js file:

import { useReducer } from "react";
import coachContext from "./coachContext";
import coachReducer from "./coachReducer";

const CoachState = (props) => {
    const initialState = {
        coaches: [],
        coach: [],
        loading: false,
    };

    const [state, dispatch] = useReducer(coachReducer, initialState);

    // Function to Add coach
    // Function to Delete coach
    // Function to Set current coach
    // Function to clear current coach
    // Function to Update coach

    return (
        <coachContext.Provider
            value={{
                coaches: state.coaches,
                coach: state.coach,
                loading: state.loading,
            }}
        >
            {props.children}
        </coachContext.Provider>
    );
};

export default CoachState;

The same goes for Athletes context, Courts context and all other elements of my application.

Finally, in my App.js I have:

import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Home from "./pages/Home";
import Coaches from "./pages/Coaches";
import Athletes from "./pages/Athletes";
import Courts from "./pages/Courts";

import CoachState from "./context/coach/CoachState";
import AthleteState from "./context/athlete/AthleteState";
import CourtState from "./context/court/CourtState";

function App() {
    return (
        <CourtState>
          <AthleteState>
            <CoachState>
              <Router>
                <Switch>
                  <Route exact path="/" component={Home}></Route>
                  <Route exact path="/coaches" component={Coaches}></Route>
                  <Route exact path="/athletes" component={Athletes}></Route>
                  <Route exact path="/courts" component={Courts}></Route>
                </Switch>
              </Router>
            </CoachState>
          </AthleteState>
        </CourtState>
    );
}

export default App;

When I finish writing my other Contexts as you can understand they will wrap the Router as all current states do. So there is going to be a big nesting "problem".

I would like any advice as to how could I resolve this nested contexts issue? Did I make the correct decision of developing my app using Context API instead of Redux?

Upvotes: 3

Views: 14093

Answers (2)

LSafer
LSafer

Reputation: 354

You could use this npm package react-pipeline-component

Your code would be like this:

import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Home from "./pages/Home";
import Coaches from "./pages/Coaches";
import Athletes from "./pages/Athletes";
import Courts from "./pages/Courts";

import CoachState from "./context/coach/CoachState";
import AthleteState from "./context/athlete/AthleteState";
import CourtState from "./context/court/CourtState";

import {Pipeline, Pipe} from 'react-pipeline-component'

function App() {
    return (
        <Pipeline components={[
          <CourtState children={<Pipe />} />,
          <AthleteState children={<Pipe />} />,
          <CoachState children={<Pipe />} />,
          <Router children={<Pipe />} />,
          <Switch children={<Pipe />} />,
          <>
            <Route exact path="/" component={Home}></Route>
            <Route exact path="/coaches" component={Coaches}></Route>
            <Route exact path="/athletes" component={Athletes}></Route>
            <Route exact path="/courts" component={Courts}></Route>
          </>
        ]}/>
    );
}

export default App;

Upvotes: 0

technolaaji
technolaaji

Reputation: 369

Instead of using multiple context providers then useContext to get each value from each context provider, you can add all of the needed values within one context provider then use a custom hook to fetch the data or function that you need

this decreases the amount of context providers used, it doesn't decrease them to 1 provider since not all logic is going to be shared or common within one provider and another

I have used Kent C. Dodds' blog post "How to use React Context effectively" as a reference to write context providers efficiently.

example: (basic counter example but I'll explain the workflow)

const MainContext = createContext(null);

const MyComponent = (props) => {
  const [counter, updateCounter] = useState(0);


  const increment = () => {
    updateCounter(counter + 1);
  }

  const decrement = () => {
   updateCounter(counter - 1);
  }

  return(
    <MainContext.Provider value={{counter, increment, decrement}}>
     {children}
   </MainContext.Provider>
  )
}

const useCountNumber = () => {
  const context = useContext(MainContext);
  
  if(context === undefined || context === null) {
    throw new Error('useCounter is not within MainContext scope');
  }
  else {
    return context.counter;
  }
}

const useIncrementCount = () => {

  const context = useContext(MainContext);
  
  if(context === undefined || context === null) {
    throw new Error('useIncrementCount is not within MainContext scope');
  }
  else {
    return context.increment;
  }
}

const useDecrementCount = () => {

  const context = useContext(MainContext);
  
  if(context === undefined || context === null) {
    throw new Error('useDecrementCount is not within MainContext scope');
  }
  else {
    return context.decrement;
  }

}



// in component you wish to use those values

const MyCounter = () => {

  const count = useCountNumber();
  const increment = useIncrementCount();
  const decrement = useDecrementCount();

  return(
   <div>
      {count}

      <button onClick={increment}> +1 </button>
      <button onClick={decrement}> -1 </button>
   </div>

);

}

I have used this in production, use one context provider and you put values inside of that single provider. This is manageable for a small set of functions but as it gets bigger then I would recommend to use something like redux or another state management library

Also consider using useMemo for memoizing some state elements and useReducer to utilize a function to optimize performance of your context if it is triggering deep updates

Upvotes: 5

Related Questions