2

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.

M Ansyori
  • 429
  • 6
  • 21

1 Answers1

0

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

Ahmed Lazhar
  • 718
  • 4
  • 10
  • thanks a lot, removing the `getCarouselData` from useEffect dependency array solved my problem, by the way is this a good approach to set the loader as a global state and can be changed by any method that requires loading dialog screen shows up? – M Ansyori Jul 28 '22 at 04:55
  • there is a problem in this approach, when two actions are run in parallel, when one is done it sets the loading to false while the other one is still running. please check the answer update, it is better to not call the appStore hook on the updater since it is a new instance on each state update and every dispatch will cause all your components to rerender. So the ref solution should be used to reduce these unnecessary rerenders – Ahmed Lazhar Jul 28 '22 at 04:59
  • I see, thanks for your opinion. I have tried your last updated answer code, it's working if I put the HomeContainer inside the AppContainer directly, but if I put it inside a Route (v5.3.3), it re-renders again to an infinite loop. Do you know why is this happening? – M Ansyori Jul 28 '22 at 05:02
  • removing the setLoading and appStore call inside the home slice eliminate the re-render problem when the HomeContainer inside a Route component. I guess I'll use each loader in every container, not just a single global loader in apps container – M Ansyori Jul 28 '22 at 05:07
  • just changing `const getCarouselData = appStore((state: any) => state.getCarousels)` by `const getCarouselsRef = useRef(appStore.getState().getCarousels)` and `getCarouselData()` by `getCarouselsRef.current()` brings back the rerendering problem ? – Ahmed Lazhar Jul 28 '22 at 05:10
  • yes, the `const getCarouselsRef = useRef(appStore.getState().getCarousels)` and `getCarouselsRef.current();` did not work if the HomeContainer is inside a component – M Ansyori Jul 28 '22 at 05:13
  • please make changes on this codesandbox to show the rerender problem because it works for me: https://codesandbox.io/s/zustand-playground-forked-iqy16m – Ahmed Lazhar Jul 28 '22 at 05:30
  • okay, I'm editing the sandbox now – M Ansyori Jul 28 '22 at 05:36
  • 1
    I was able to solve the re-render issue by following your code, here's the edited sandbox code https://codesandbox.io/s/pedantic-feistel-p1eu5r thank you very much for your help – M Ansyori Jul 28 '22 at 06:53