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

1

8

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.

Tautology answered 30/1, 2020 at 17:25 Comment(6)
Not sure why you create the context in the parent and not outside of it... (or why wrap it with useMemo)Pricilla
I agree with @Sagivb.g. Why you want to keep it inside the component? Just move it outiside, export it and import it by other componentsFilippa
context is not just for parent to children, can be used anywhere. just think it as a state wrapper.Buddhi
The point is that there's nothing globally unique, like each Accordion maintains its own state of which sections are open. Multiple instances of the accordion require their own context. But the nature of React context is that you can't just instantiate new context instances on the fly when a "root" component gets rendered.Tautology
I think this is just not something that can be practically done in React. For example, Redux requires you to specify the name of the store if you want more than Redux store in the app, on every connected component which reads from the non-default store. So I think the same methodology is required, i.e. if a component serves multiple contexts, the children need to provide the unique identifier, rather than just having magic access to the context of its nearest provider parent. Made a POC here: codesandbox.io/s/sweet-hofstadter-b42iyTautology
I have a situation where I'm returning frozen components from a hook, with typescript generics being applied to all components. I am calling createContext in my hook initialization, give to a useRef as it's initial value, and then passing that context to the provider (a Form thing) and consumers (TextField's, Select's, etc). Bottom line is, createContext is just a function, you can use it as you see fit. However, I think Provider's need to be referentially stable in the dom. No callbacks returning providers (unless you useCallback(.., []) to 'freeze' it.Evyn
P
9

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

Pricilla answered 30/1, 2020 at 17:45 Comment(1)
Thank you, I was missing something completely obvious! I was under the impression that createContext() created a singular container that held a singular context value. I didn't realize until just now that each Provider has its own value; therefore, multiple accordions would use the same createContext() object but each accordion would have its own context data as long as each is wrapped in its own provider. That makes a ton of sense and makes this possible the "normal" way. Thanks!Tautology

© 2022 - 2024 — McMap. All rights reserved.