const initialState = {
productEditor: {
productDetails: {
id: 1,
name: "Product A",
description: "A great product",
},
tags: ["tag1", "tag2"],
images: {
main: "image1.png",
gallery: ["image2.png", "image3.png"],
},
},
};
Using React-Redux selectors suppose one component subscribes to productDetails.name
and another subscribes to images.main
but I change productDetails.name
, will this cause other components to re-render? Is it possible to divide a state into categories and only subscribe to any depth property avoiding unnecessary re-renders? As I understand, Redux compares references so if I just reassign an existing object its ref wont change so that should not cause re-render.
Using React-Redux selectors suppose one component subscribes to
productDetails.name
and another subscribes toimages.main
but I changeproductDetails.name
will this cause other components to re-render?
No, only when what the useSelector
hook is selecting changes does this trigger the subscribed component to rerender. In other words, components only rerender when what they are subscribed to changes. If one component is subscribed to productDetails.name
and another is subscribed to images.main
, then state updates specifically to productDetails.name
does not trigger the component subscribed to images.main
to rerender, and vice-versa.
Consider this demo sandbox:
Demo Code:
index.js
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "./store";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>
);
store.js
import { configureStore } from "@reduxjs/toolkit";
import stateReducer from "./state.slice";
export const store = configureStore({
reducer: stateReducer,
});
state.slice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
productEditor: {
productDetails: {
id: 1,
name: "Product A",
description: "A great product",
},
tags: ["tag1", "tag2"],
images: {
main: "image1.png",
gallery: ["image2.png", "image3.png"],
},
},
};
const stateSlice = createSlice({
name: "state",
initialState,
reducers: {
updateProductName: (state, action) => {
state.productEditor.productDetails.name = action.payload;
},
updateImagesMain: (state, action) => {
state.productEditor.images.main = action.payload;
},
},
});
export const { updateImagesMain, updateProductName } = stateSlice.actions;
export default stateSlice.reducer;
App.js
import { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { nanoid } from "@reduxjs/toolkit";
import { updateImagesMain, updateProductName } from "./state.slice";
const ComponentA = () => {
const dispatch = useDispatch();
const productDetails = useSelector(
(state) => state.productEditor.productDetails
);
const renderCount = useRef(0);
useEffect(() => {
console.log("ComponentA RENDERED");
renderCount.current++;
});
return (
<div>
<h1>Product Details</h1>
<button
type="button"
onClick={() => dispatch(updateProductName(nanoid()))}
>
Update Product Name
</button>
<p>{JSON.stringify(productDetails)}</p>
<p>Render Count: {renderCount.current}</p>
</div>
);
};
const ComponentB = () => {
const dispatch = useDispatch();
const images = useSelector((state) => state.productEditor.images);
const renderCount = useRef(0);
useEffect(() => {
console.log("ComponentB RENDERED");
renderCount.current++;
});
return (
<div>
<h1>Images</h1>
<button
type="button"
onClick={() => dispatch(updateImagesMain(nanoid()))}
>
Update Images Main
</button>
<p>{JSON.stringify(images)}</p>
<p>Render Count: {renderCount.current}</p>
</div>
);
};
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<ComponentA />
<ComponentB />
</div>
);
}
Because of the way React-Redux is optimized via the useSelector
hook you'll want to be as granular and specific as to the state values you are selecting. Selecting more than you actually need may trigger unnecessary component re-renders. For example, if both components subscribed to state.productEditor
instead of the more deeply nested states, then any time state.productEditor.productDetails.*
or state.productEditor.images.*
updated, both subscribers would re-render.
const { productDetails } = useSelector(state => state.productEditor);
const { images } = useSelector(state => state.productEditor);
Updates to anything in state.productEditor
will cause both these susbcribers to re-render.