Search code examples
javascriptreactjszustand

Infinite re-render using Zustand


I'm coming from redux + redux-saga and class component, everything went well when using componentDidMount in class component. Dispatching action to fetch api works well without duplicate request.

I've been learning functional component for a while, and decided to use Zustand to replace my redux-saga to handle my state management process. I've able to set state A from state B in reducer just by calling the action creators and the state get updated.

First of all, here's my react functional component code so far:

HomeContainer

import { useEffect } from "react";
import { appStore } from "../App/store";

export default function HomeContainer(props: any): any {
  const getCarouselData = appStore((state: any) => state.getCarousels);
  const carousels = appStore((state: any) => state.carousels);

  useEffect(() => {
    if (carousels.length === 0) {
      getCarouselData();
    }
  }, [carousels, getCarouselData]);

  console.log("carousels", carousels);

  return <p>Home Container</p>;
}

Loading Slice

const loadingSlice = (set: any, get: any) => ({
  loading: false,
  setLoading: (isLoading: boolean) => {
    set((state: any) => ({ ...state, loading: isLoading }));
  },
});

export default loadingSlice;

App Store

import create from "zustand";
import homeSlice from "../Home/store";
import loadingSlice from "../Layout/state";

export const appStore = create((set: any, get: any) => ({
  ...loadingSlice(set, get),
  ...homeSlice(set, get),
}));

Coming to Zustand, it seems like the behaviour is different than Redux. I'm trying to update the boolean value of loading indicator with this code below:

import create, { useStore } from "zustand";
import axios from "axios";
import { appStore } from "../App/store";

const homeSlice = (set: any, get: any) => ({
  carousels: [],
  getCarousels: () => {
    appStore.getState().setLoading(true);
    axios
      .get("api-endpoint")
      .then((res) => {
        set((state: any) => ({
          ...state,
          carousels: res.data,
        }));
      })
      .catch((err) => {
        console.log(err);
      });
    appStore.getState().setLoading(true);
  },
});

export default homeSlice;

The state is changing, the dialog is showing, but the component keeps re-render until maximum update depth exceeded. I have no idea why is this happening. How can I update state from method inside a state without re-rendering the component?

Any help will be much appreciated. Thank you.


Solution

  • Update

    The new instance of getCarousels is not created because of the dispatch since the create callback is called only once to set the initial state, then the updates are made on this state.

    Original answer

    Your global reducer is calling homeSlice(set, get) on each dispatch (through set). This call creates a new instance of getCarousels which is passed as a dependency in your useEffect array which causes an infinite re-rendering.

    Your HomeContainer will call the initial getCarousels, which calls setLoading which will trigger the state update (through set) with a new getCarousels. The state being updated will cause the appStore hook to re-render the HomeContainer component with a new instance of getCarousels triggering the effect again, in an infinite loop.

    This can be solved by removing the getCarouselsData from the useEffect dependency array or using a ref to store it (like in the example from the Zustand readme) this way :

      const carousels = appStore((state: any) => state.carousels);
      const getCarouselsRef = useRef(appStore.getState().getCarousels)
      useEffect(() => appStore.subscribe(
        state => (getCarouselsRef.current = state.getCarousels)
      ), [])
      useEffect(() => {
        if (carousels.length === 0) {
          getCarouselsRef.current();
        }
      }, [carousels]); // adding getCarouselsRef here has no effect