Let's say I want to create a UI ponent for an "accordion" (a set of collapsible panels). The parent ponent 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 acplishing this would be to have Accordion
run an effect whenever children
changes, then traverse children
deeply and find AccordionSection
ponents, while not recursing any nested Accordion
ponents -- 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
.
Let's say I want to create a UI ponent for an "accordion" (a set of collapsible panels). The parent ponent 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 acplishing this would be to have Accordion
run an effect whenever children
changes, then traverse children
deeply and find AccordionSection
ponents, while not recursing any nested Accordion
ponents -- 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
.
-
3
Not sure why you create the context in the parent and not outside of it... (or why wrap it with
useMemo
) – Sagiv b.g Commented Jan 30, 2020 at 17:29 - 1 I agree with @Sagivb.g. Why you want to keep it inside the ponent? Just move it outiside, export it and import it by other ponents – Vencovsky Commented Jan 30, 2020 at 17:36
- context is not just for parent to children, can be used anywhere. just think it as a state wrapper. – Bhojendra Rauniyar Commented Jan 30, 2020 at 17:37
- 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" ponent gets rendered. – M Miller Commented Jan 31, 2020 at 18:03
- 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 ponent which reads from the non-default store. So I think the same methodology is required, i.e. if a ponent 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-b42iy – M Miller Commented Jan 31, 2020 at 19:51
1 Answer
Reset to default 9React.createContext
will return an object that holds 2 ponents:
- Provider
- Consumer
These 2 ponents 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 ponent and use it to render a Consumer
inside your children
ponents (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