Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there any practical way to call `React.createContext()` within a component?

Let's say I want to create a UI component for an "accordion" (a set of collapsible panels). The parent component controls the state of which panels are open, while the child panels should be able to read the context to determine whether or not they're open.

const Accordion = ({ children }) => {
  const [openSections, setOpenSections] = useState({})

  const isOpen = sectionId => Boolean(openSections[sectionId])

  const onToggle = sectionId => () =>
    setOpenSections({ ...openSections, [sectionId]: !openSections[sectionId] })

  const context = useMemo(() => createContext(), [])
    // Can't tell children to use *this* context

  return (
    <context.Provider value={useMemo(() => ({ isOpen, onToggle }), [isOpen, onToggle])}>
      {children}
    </context.Provider>
  )
}

const AccordionSection = ({ sectionId, title, children }) => {
  const { isOpen, onToggle } = useContext(context)
    // No way to infer the right context

  return (
    <>
      <button onClick={onToggle(sectionId)}>{isOpen(sectionId) ? 'Close' : 'Open'}</button>
      {isOpen && children}
    </>
  )
}

The only way I could think of accomplishing this would be to have Accordion run an effect whenever children changes, then traverse children deeply and find AccordionSection components, while not recursing any nested Accordion components -- then cloneElement() and inject context as a prop to each AccordionSection.

This seems not only inefficient, but I'm not even entirely sure it will work. It depends on children being fully hydrated when the effect runs, which I'm not sure if that happens, and it also requires that Accordion's renderer gets called whenever deep children change, which I'm not sure of either.

My current method is to create a custom hook for the developer implementing the Accordion. The hook returns a function which returns the isOpen and onToggle functions which have to manually be passed to each rendered AccordionSection. It works and is possibly more elegant than the children solution, but requires more overhead as the developer needs to use a hook just to maintain what would otherwise be state encapsulated in Accordion.

like image 222
M Miller Avatar asked Jan 30 '20 17:01

M Miller


1 Answers

React.createContext will return an object that holds 2 components:

  1. Provider
  2. Consumer

These 2 components can share data, the Consumer can "grab" the context data from the nearest Provider up the tree (or use the useContext hook instead of rendering a Consumer).

You should create the context object outside the parent component and use it to render a Consumer inside your children components (or use the useContext hook).

Simple example:

const myContext = createContext();

const Accordion = ({ children }) => {
  // ...
  return (
    <myContext.Provider value={...} >
      {children}
    </myContext.Provider>
  )
}


const AccordionSection = (...) => {
  const contextData = useContext(myContext);
  // use the data of your context as you wish
  // ...
}

Note that i used the useContext hook instead of rendering the Consumer, its up to you if you want to use the hook or the Consumer.

You can see more examples and get more details from the docs

like image 69
Sagiv b.g Avatar answered Oct 22 '22 23:10

Sagiv b.g