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.
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 . This call creates a new instance of homeSlice(set, get)
on each dispatch (through set
)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