I was wondering how do these UI libraries like Headless UI, React Aria Components, etc. just pass a list of Elements, for e.g. <Tab>
components in <TabList>
and <TabPanel>
in <TabPanels>
component (in case of Headless UI Tab Components) and they all just work without setting any state in the parent component where we are importing these Components.
I tried to create similar component earlier as well but I don't know how to get a list of child Elements or Components to be set in an array or object in context of it's parent component. For example if I will create a <TabGroup>
component similar to the Headless UI's <TabGroup>
component then I would use it in my project like below.
<TabGroup>
<TabList>
<TabButton>Tab 1</TabButton>
<TabButton>Tab 2</TabButton>
<TabButton>Tab 3</TabButton>
</TabList>
<TabPanels>
<TabPanel>Panel 1 Content</TabPanel>
<TabPanel>Panel 2 Content</TabPanel>
<TabPanel>Panel 3 Content</TabPanel>
</TabPanels>
</TabGroup>
I just don't keep these tabs in an array and then map through them or pass them as props to the <TabGroup>
component or any of it's children. I just declare them just like any other HTML elements.
Now what I can do is create a context in <TabGroup>
component and set activeTab
property in it which will be updated every time a click on one of the <TabButton>
components. But now how do I know which tab to display when activeTab
is set to 0
or 1
or any other value?
Also what if I want an array of tabButtons
and an array of tabPanels
in the context that I created in <TabGroup>
component?
I have used tab example because I want to create one right now but I was facing this issue while creating custom <Select>
component also and many other components.
I am not using these 3rd party libraries due to some reasons:
Please help me with this.
Thank you in advance.
I was thinking to set the activeTab
property in context to the index of the button that is being clicked but how do I get to know the index
of the button being clicked?
The short answer is: Context
and yes, you should probably use these libraries.
The long answer:
Let's look at the code for Tabs
in react-aria
: https://github.com/adobe/react-spectrum/blob/main/packages/react-aria-components/src/Tabs.tsx
On line 316 we see this:
return (
<div
{...domProps}
ref={ref}
data-focused={isFocused || undefined}
data-focus-visible={isFocusVisible || undefined}
// @ts-ignore
inert={inertValue(!isSelected)}
data-inert={!isSelected ? 'true' : undefined}>
<Provider
values={[
[TabsContext, null],
[TabListStateContext, null]
]}>
<CollectionRendererContext.Provider value={DefaultCollectionRenderer}>
{renderProps.children}
</CollectionRendererContext.Provider>
</Provider>
</div>
);
Now it's not important what exactly the Context
does or is. But we know that whenever a TabPanel
is rendered, its children are wrapped in a Context Provider
. So the inner children will be able to use that context. As we see on line 204:
let state = useContext(TabListStateContext)!;
The reason to use these libraries is because they handle a lot of edge cases. For instance making sure everything works fine on mobile. Or in the case of react-aria
it handles a lot of accessibility features for you. You would have to re-implement all those things yourself. While the libraries give them to you for free.
In any case, the basis of building a component like this looks like this:
const TabListContext = React.createContext({
activeTab: 0,
setActiveTab: (index) => {}
})
const TabGroup = (props) => {
const { children } = props
const [activeTab, setActiveTab] = React.useState(0)
// add index prop to each child
const tabs = React.Children.map(children, (child, index) => {
return React.cloneElement(child, { index })
})
return (<div>
<TabListContext.Provider value={{ activeTab, setActiveTab }}>
{ tabs }
</TabListContext.Provider>
</div>)
}
const Tab = (props) => {
const { activeTab, setActiveTab } = React.useContext(TabListContext)
const { index, title } = props
const isActive = activeTab === index
return (<div onClick={() => setActiveTab(index)}>
{title} ({ isActive ? 'Active' : 'Inactive' })
</div>)
}
const App = () => {
return (<TabGroup>
<Tab title="First tab" />
<Tab title="Second tab" />
</TabGroup>)
}
ReactDOM.render(<App />, document.getElementById("container"))
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<div id="container"></div>
Run snippet and click on a tab to see active state change