Search code examples
javascriptreactjstypescriptnext.js13

how to handle dynamically created tabs and their state in NextJS


I have a requirement where i need to create a web app in nextjs that has tabs components in it.

The idea is that this will handle multiple entities in each tab, for example i have a search bar to select diferent products, when i select one, a new tab is created with that product info as well as forms which the user will manually fill.

Each time i select a new product a new tab is opened but i should not loose what i entered in the first tab.

I need to be able to switch between tabs without loosing state.

I want to know is there any simple straight forward way? a simple example will be great.

I have achieved this using zustand and store each tab content changes in a variable then fetch it when ever a tab is opened, but it looks dirty and not maintainable.


Solution

  • Handling dynamically created tabs and their state in Next.js can be streamlined using React's state management and context API.

    First, create a context to manage the state of the tabs.

    // contexts/TabsContext.js
    import { createContext, useContext, useState } from 'react';
    
    const TabsContext = createContext();
    
    export const useTabs = () => useContext(TabsContext);
    
    export const TabsProvider = ({ children }) => {
      const [tabs, setTabs] = useState([]);
      const [activeTab, setActiveTab] = useState(null);
    
      const addTab = (tab) => {
        setTabs([...tabs, tab]);
        setActiveTab(tab.id);
      };
    
      const removeTab = (id) => {
        setTabs(tabs.filter(tab => tab.id !== id));
        if (activeTab === id) {
          setActiveTab(tabs.length > 0 ? tabs[0].id : null);
        }
      };
    
      const switchTab = (id) => {
        setActiveTab(id);
      };
    
      return (
        <TabsContext.Provider value={{ tabs, activeTab, addTab, removeTab, switchTab }}>
          {children}
        </TabsContext.Provider>
      );
    };
    

    This component will handle the rendering of tabs and the forms inside each tab.

    // components/Tabs.js
    import { useTabs } from '../contexts/TabsContext';
    
    const Tabs = () => {
      const { tabs, activeTab, addTab, removeTab, switchTab } = useTabs();
    
      return (
        <div>
          <div className="tab-list">
            {tabs.map(tab => (
              <button key={tab.id} onClick={() => switchTab(tab.id)}>
                {tab.title}
                <span onClick={() => removeTab(tab.id)}>x</span>
              </button>
            ))}
            <button onClick={() => addTab({ id: Date.now(), title: 'New Tab', content: '' })}>
              + New Tab
            </button>
          </div>
          <div className="tab-content">
            {tabs.map(tab => (
              tab.id === activeTab ? <div key={tab.id}>{tab.content}</div> : null
            ))}
          </div>
        </div>
      );
    };
    
    export default Tabs;
    

    Each tab will contain a form whose data needs to be persisted.

    // components/TabForm.js
    import { useState } from 'react';
    import { useTabs } from '../contexts/TabsContext';
    
    const TabForm = ({ tabId }) => {
      const { tabs, setTabs } = useTabs();
      const tab = tabs.find(tab => tab.id === tabId);
      const [formData, setFormData] = useState(tab ? tab.content : '');
    
      const handleChange = (e) => {
        setFormData(e.target.value);
        setTabs(tabs.map(tab => tab.id === tabId ? { ...tab, content: e.target.value } : tab));
      };
    
      return (
        <form>
          <textarea value={formData} onChange={handleChange} />
        </form>
      );
    };
    
    export default TabForm;
    

    Wrap your main application component with the TabsProvider to provide the context to all components.

    // pages/_app.js
    import { TabsProvider } from '../contexts/TabsContext';
    
    function MyApp({ Component, pageProps }) {
      return (
        <TabsProvider>
          <Component {...pageProps} />
        </TabsProvider>
      );
    }
    
    export default MyApp;
    

    Finally, use the Tabs component in a page to see it in action.

    // pages/index.js
    import Tabs from '../components/Tabs';
    
    export default function Home() {
      return (
        <div>
          <h1>Dynamic Tabs Example</h1>
          <Tabs />
        </div>
      );
    }