I'm working on a complex analytics dashboard built with React and TypeScript, which involves multiple nested components. I'm using a combination of state management techniques including React Context, Zustand, and useReducer. The dashboard fetches data from an API and needs to update several deeply nested components based on user interaction and incoming data.
However, I noticed performance issues where certain user actions trigger excessive re-renders, significantly impacting performance. Particularly, when I update a specific piece of state in my context provider, even unrelated nested components re-render unnecessarily.
// this is Dashboard.tsx
function Dashboard() {
return (
<MainProvider>
<Layout>
<Sidebar />
<MainContent />
</Layout>
</MainProvider>
);
}
// MainProvider.tsx
const MainContext = createContext(null);
function MainProvider({ children }) {
const [filters, setFilters] = useState({});
const [data, dispatch] = useReducer(dataReducer, initialData);
const contextValue = useMemo(() => ({ filters, setFilters, data, dispatch }), [filters, data]);
return (
<MainContext.Provider value={contextValue}>
{children}
</MainContext.Provider>
);
}
In my nested components, I am consuming this context like this:
this is Sidebar.tsx
const { filters, setFilters } = useContext(MainContext);
function Sidebar() {
// ...here I render filters and triggers setFilters on interaction
}
// this is MainContent.tsx
function MainContent() {
const { data } = useContext(MainContext);
return (
<div>
<DataChart data={data.chart} />
<DataTable data={data.table} />
</div>
);
}
When I update the filters state via setFilters in Sidebar, it seems to cause MainContent, DataChart, and DataTable to re-render unnecessarily even if their props haven't changed.
I have tried using React.memo around child components (DataChart, DataTable) to prevent unnecessary renders. also splitting context into multiple contexts to minimize shared state.
Why are unrelated components re-rendering despite memoization and a good and careful state management?
How can I better structure my context/state management to ensure updates only affect relevant components?
I think the main issue here is that updating state within a context provider often causes all components consuming that context to re-render, even if the specific part of the context they are using has not actually changed. This happens because the context provider re-creates its context value, leading to new object references.
When React Context value changes, every consumer of that context re-renders by default even if a consumer doesn't directly rely on the specific piece of state that changed.
In your case, the problem is likely this line in your MainProvider
const contextValue = useMemo(() => ({ filters, setFilters, data, dispatch }), [filters, data]);
Every time filters or data updates, the context provider returns a new object reference, causing all consumers to re-render.
Solutions:
1- one thing is that you can Split Contexts, Create separate contexts for different state elements to isolate consumers
const FiltersContext = createContext(null);
const DataContext = createContext(null);
This way, updates to the filters context won't trigger re-renders in components using the data context.
2- you can memoize your Context Consumers, you can Wrap components using context values with React.memo and pass down stable props explicitly.
like this:
const MemoizedDataChart = React.memo(DataChart);
const MemoizedDataTable = React.memo(DataTable);
function MainContent() {
const { data } = useContext(MainContext);
return (
<div>
<MemoizedDataChart data={data.chart} />
<MemoizedDataTable data={data.table} />
</div>
);
}