reactjscomponents

How do I store a list of items in context without passing them as props of top level component in React?


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:

  1. These don't provide individual components. I have to download whole library to just use 1 or 2 components from them.
  2. There are a few styling and markup customization limitations (as far as I felt).
  3. I also feel good if I create them on my own but please tell me if I should use these libraries or create my own components because I don't like using UI libraries and I like creating components on my own so that I feel ownership over the code 😅.

Please help me with this.

Thank you in advance.

What I tried

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?


Solution

  • 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