Search code examples
javascriptreactjsreact-redux

Will updating a single property in a deeply nested react Redux state cause re-renders for all components that subscribe to parts of that state?


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.


Solution

  • 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?

    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.

    Demo

    Consider this demo sandbox:

    Edit heuristic-grass

    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>
      );
    }
    

    Note

    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.