I’m building a tab system in NextJS. The system allows users to have multiple tabs open at the root of the UI. Each tab (module as referred to below) has an id and a pathname.
type Module = {pathname: string, id:string}
children
is the page passed to us by the NextJS app router. The component below acts as the root layout.
My intention was to store the current state of children for each module in viewComponents
. Each time the pathname changes, we set the currently focused tab’s view component to the latest children.
However, when we update the children for the currently focused tab, all the tabs seem to update with the new children. I assumed this was a shallow copy issue so I used React.cloneElement to clone each iteration of children before storing it in viewComponents. This did not work and the same result prevailed.
Here is my code:
"use client"
import React, { PropsWithChildren, useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
import Randomstring from 'randomstring'
const generateModuleID = () => Randomstring.generate(8)
const Tab = ({ module, focusedID, setFocusedID, component }: { module: Module, focusedID: string, setFocusedID: (id: string) => void, component: React.ReactNode }) => {
const [focused, setFocused] = useState(false)
useEffect(() => {
if (module.id === focusedID) {
setFocused(true)
} else {
setFocused(false)
}
}, [module, focusedID])
return (
<Box onClick={() => {
!focused ? setFocusedID(module.id) : void 0
}} style={{ borderRadius: 10, flex: 1, overflow: 'hidden', border: "1px solid rgba(255,255,255,.1)" }}>
<Box className='flex aic jcc' py={5} bg={focused ? "#fff" : "dark.6"} style={{ width: "100%" }}>
<Text fz="sm" fw={600} c={focused ? "#212121" : "dimmed"}>Tab {module.id}</Text>
</Box>
<Box style={{ flex: 1, overflowY: "auto", height: "100%" }} p="sm">
{component}
</Box>
</Box>
)
}
export default function ModularManager({ children }: PropsWithChildren) {
const pathname = usePathname()
const [focusedView, setFocusedView] = useState("")
const [views, setViews] = useState<Array<Module>>([])
const [viewComponents, setViewComponents] = useState<Record<string, React.ReactNode>>({})
useEffect(() => {
const newViews = [...views]
const newID = generateModuleID()
if (!newViews.find((v) => v.id === focusedView)) {
newViews.push({
pathname: pathname,
id: newID
})
setFocusedView(newID)
} else {
const idx = newViews.findIndex((v) => v.id === focusedView)
newViews[idx].pathname = pathname
setFocusedView(newViews[idx].id)
}
setViews(newViews)
}, [pathname])
useEffect(() => {
if (!focusedView) return;
if (viewComponents[focusedView]) return;
setViewComponents((prev) => ({ ...prev, [focusedView]: React.cloneElement(children, {}) }))
}, [pathname])
return (
<div className='flex' style={{ width: '100%', gap: 10, overflow: "hidden", height: "calc(100vh - 120px)" }}>
<Button onClick={() => setViews((prev) => [...prev, {
id: generateModuleID(),
pathname: "/",
component: null
}])}>Add</Button>
{views.map((v, i) => <Tab key={i} component= {viewComponents[v.id]} setFocusedID={setFocusedView} focusedID={focusedView} module={v} />)}
</div>
)
}
Any help would be appreciated. Many thanks.