Search code examples
reactjsjsxreact-context

A Provider component that combines React Context and Reducer and uses {children} prop cannot be easily used by parent components with complex logic


Following the React tutorial on combining reducers with Context, I have packaged up a reducer into a Provider component:

export function CoursesModeProvider({ children, coursesDB }) {
  const [courses, dispatch] = useReducer(
    coursesModeReducer, 
    { all: coursesDB, displayed: coursesDB, editing: null }
  );

  return (
    <CoursesModeContext.Provider value={courses}>
      <CoursesModeDispatchContext.Provider value={dispatch}>
        {children}
      </CoursesModeDispatchContext.Provider>
    </CoursesModeContext.Provider>
  );
}

But what if the parent component that needs to use this CoursesModeProvider has complex state logic that involves ternary expressions? It appears that I then have to do something like this in the component's render code if I want the reducer to be accessible by child components:

return (
  (showAddCourseDialog)
    ?  <CoursesModeProvider coursesDB={coursesDB}>
         <CoursesModeAdd close={closeAddCourseDialog} />
       </CoursesModeProvider>
    : (showCourseDetailsDialog)
      ? <CoursesModeProvider coursesDB={coursesDB}>
          <CoursesModeDetails close={closeCourseDetailsDialog} />
        </CoursesModeProvider>
      : <CoursesModeProvider coursesDB={coursesDB}>
          <h1 className="centered">Courses</h1>
          <CoursesModeSearchFilter />
          <CoursesModeTable showCourseDetails={openCourseDetailsDialog} />
          <button className="float-btn" onClick={openAddCourseDialog}>
            <FontAwesomeIcon icon="map-pin" />
            &nbsp;Add Course
          </button>
        </CoursesModeProvider>
  );
}

Ouch! Every conditional JSX block needs to be packaged in a <CoursesModeProvider>! I am concerned that this is inefficient because a new reducer needs to be spun up on each state change.

An alternative appears to be not to package the context and reducer into a provider, and instead declare the reducer locally within the parent component. Then I think I could do something like this:

return (
  <CoursesModeContext.Provider value={courses}>
    <CoursesModeDispatchContext.Provider value={dispatch}>
      (showAddCourseDialog)
        ? <CoursesModeAdd close={closeAddCourseDialog} />
        : (showCourseDetailsDialog)
          ? <CoursesModeDetails close={closeCourseDetailsDialog} /> 
          : <>
              <h1 className="centered">Courses</h1>
              <CoursesModeSearchFilter />
              <CoursesModeTable showCourseDetails={openCourseDetailsDialog} />
              <button className="float-btn" onClick={openAddCourseDialog}>
                <FontAwesomeIcon icon="map-pin" />
                &nbsp;Add Course
              </button>
            </>
    </CoursesModeDispatchContext.Provider>
  </CoursesModeContext.Provider>
);

Which is preferrable and why?


Solution

  • It's not necessary to repeat the CoursesModeProvider component in each branch. You can use the ternary operator to choose only the children for the component.

    return (
        <CoursesModeProvider coursesDB={coursesDB}>
            {showAddCourseDialog ? <CoursesModeAdd close={closeAddCourseDialog} /> :
             showCourseDetailsDialog ? <CoursesModeDetails close={closeCourseDetailsDialog} /> :
             <>
                <h1 className="centered">Courses</h1>
                <CoursesModeSearchFilter />
                <CoursesModeTable showCourseDetails={openCourseDetailsDialog} />
                <button className="float-btn" onClick={openAddCourseDialog}>
                <FontAwesomeIcon icon="map-pin" />
                &nbsp;Add Course
                </button>
             </>}
        </CoursesModeProvider>
    );