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
.
React.createContext
will return an object that holds 2 components:
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With