I am developing React web SPA ERP application with dynamic mulitabbed interface and I am having problems with extra-rerenderings. I am thinking about different options (e.g. my question about component design https://softwareengineering.stackexchange.com/questions/455372/singe-page-application-for-the-web-with-dynamic-tabbed-interface-is-there-ne/455375) and I am doing profiling of my code and I have arrived at this situation:
My application is: the user can open new in-app tabs, each new form is opened as respective component-view in its own tab. I tested my application: I opened my application with the default Dashboard tab visible then I clicked on the link and opened other view in its onw in-application tab and then I switched back to the Dashboard tab and back to the new tab. TabPanel (key=0) is the TabPanel component that hosts Dashboard component.
So - clicking on the link, swithcing between tabs - all this is causing url changes (for the same domain, of course, this if SPA app after all) and url changes are processed in a manner that causes changes to the data (tabs array and activeTabId variable) and data ar rendered into Tab's and TabPanel's.
Of course - each url change and activeTabId may cause and causes re-renders, including re-render of TabPanel key=0, that is OK. But the strange situation is, that after each re-render due to url (react-router-dom location) change and re-render after activeTabId change, there are 4 re-renders of TabPanel key=0 due to "Why did this render?"="The parent component rendered" - see picture. And the Flamegraphs shows no re-rendered parent for any of those 4 re-rerenders! This is really strange. How the component may be re-rendered due to parent re-render, if there is no re-rendered parent. I have checked the parent re-renders and they count was minimal. Can ir be possible that for one parent re-render (e.g. due to url/location change or activeTabId change) there can be following 4 extra re-renders of the children to the parent?
I am giving the full code of the minimal app (I summarized it from my production app but I have not tested this summarized app, it illustrates my approach: 1) any view open or switching between tabs goes through url changes, 2) that are handled by the router, saved into data and 3) Tab's/TabPanel's are rendered from those data dynamically) at the end, but here is the crux of rendering of TapPabel's - maybe this rendering is somehow inefficient and introduces parent problems?
{tabs.map((tab) => (<TabPanel key={tab.id} tab_hidden={activeTabId !== tab.id}> {tab.component}</TabPanel> ))}
Here is one-pager with full (summarized, essential code):
import React, { useState, useEffect, useCallback, useMemo } from 'react';import { BrowserRouter, Routes, Route, useNavigate, useLocation } from 'react-router-dom';import { Tabs, Tab, Box } from '@mui/material';import CloseIcon from '@mui/icons-material/Close';// Closable Tab componentconst ClosableTab = React.memo(({ label, onClose, closable }) => (<Box sx={{ display: 'flex', alignItems: 'center' }}><span>{label}</span> {closable && (<CloseIcon onClick={(e) => { e.stopPropagation(); onClose(); }} sx={{ ml: 1, cursor: 'pointer', fontSize: 'small' }} /> )}</Box>));// TabPanel componentconst TabPanel = React.memo(({ children, tab_hidden }) => (<div role="tabpanel" hidden={tab_hidden} style={{ display: tab_hidden ? 'none' : 'flex', flexDirection: 'column', flex: 1 }}><Box sx={{ p: 3 }}>{children}</Box></div>));// Create a new tabconst createTab = (id, path, label, component, closable = true) => ({ id, path, label, component, closable,});const App = () => { return (<BrowserRouter><MainApp /></BrowserRouter> );};// MainApp Component with Tab and Routing Logicconst MainApp = () => { const navigate = useNavigate(); const location = useLocation(); const [tabs, setTabs] = useState([ createTab(0, '/dashboard', 'Dashboard', <DashboardView />, false), ]); const [activeTabId, setActiveTabId] = useState(0); useEffect(() => { const currentPath = location.pathname; const foundTab = tabs.find((tab) => tab.path === currentPath); if (foundTab) { setActiveTabId(foundTab.id); } }, [location.pathname, tabs]); const handleTabChange = (event, newValue) => { const selectedTab = tabs.find((tab) => tab.id === newValue); if (selectedTab) { navigate(selectedTab.path); } setActiveTabId(newValue); }; const addTab = (path, label, component) => { const newId = tabs.length; const newTab = createTab(newId, path, label, component); setTabs((prev) => [...prev, newTab]); setActiveTabId(newTab.id); navigate(path); }; const closeTab = (tabId) => { setTabs((prev) => prev.filter((tab) => tab.id !== tabId)); setActiveTabId((prev) => (prev === tabId ? 0 : prev)); // Switch to Dashboard if closing active navigate('/dashboard'); }; const memoizedTabs = useMemo( () => tabs.map((tab) => (<Tab key={tab.id} value={tab.id} label={<ClosableTab label={tab.label} onClose={() => closeTab(tab.id)} closable={tab.closable} />} /> )), [tabs] ); return (<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', flex: 1, overflow: 'hidden' }}><Tabs value={activeTabId} onChange={handleTabChange} variant="scrollable" scrollButtons="auto"> {memoizedTabs}</Tabs> {tabs.map((tab) => (<TabPanel key={tab.id} tab_hidden={activeTabId !== tab.id}> {tab.component}</TabPanel> ))}<Box sx={{ p: 2 }}><button onClick={() => addTab('/tab1', 'Tab 1', <ExampleView id={1} />)}>Open Tab 1</button><button onClick={() => addTab('/tab2', 'Tab 2', <ExampleView id={2} />)}>Open Tab 2</button></Box> {/* Routes for different views */}<Routes><Route path="/dashboard" element={<DashboardView />} /><Route path="/tab1" element={<ExampleView id={1} />} /><Route path="/tab2" element={<ExampleView id={2} />} /></Routes></Box> );};// Example viewsconst DashboardView = React.memo(() => (<div><h2>Dashboard</h2><p>This is the dashboard view.</p></div>));const ExampleView = React.memo(({ id }) => (<div><h2>Tab {id}</h2><p>This is content for tab {id}.</p></div>));export default App;