Too many re-renders with useSelector hook closure - reactjs

Considering this state, I need to select some data from it:
const initialState: PlacesStateT = {
activeTicket: null,
routes: {
departure: {
carriageType: 'idle',
extras: {
wifi_price: 0,
linens_price: 0,
},
},
arrival: {
carriageType: 'idle',
extras: {
wifi_price: 0,
linens_price: 0,
},
},
},
};
so, I came up with two approaches:
first:
const useCoaches = (dir: string) => {
const name = mapDirToRoot(dir);
const carType = useAppSelector((state) => state.places.routes[name].carriageType);
const infoT = useAppSelector((state) => {
return state.places.activeTicket.trainsInfo.find((info) => {
return info.routeName === name;
});
});
const coaches = infoT.trainInfo.seatsTrainInfo.filter((coach) => {
return coach.coach.class_type === carType;
});
return coaches;
};
and second:
const handlerActiveCoaches = (name: string) => (state: RootState) => {
const { carriageType } = state.places.routes[name];
const { activeTicket } = state.places;
const trainInfo = activeTicket.trainsInfo.find((info) => {
return info.routeName === name;
});
return trainInfo.trainInfo.seatsTrainInfo.filter((coach) => {
return coach.coach.class_type === carriageType;
});
};
const useActiveInfo = (dir: string) => {
const routeName = mapDirToRoot(dir);
const selectActiveCoaches = handlerActiveCoaches(routeName);
const coaches = useAppSelector(selectActiveCoaches);
return coaches;
};
Eventually, if the first one works ok then the second one gives a lot of useless re-renders in component. I suspect that there are problems with selectActiveCoaches closure, maybe react considers that this selector is different on every re-render but I am wrong maybe. Could you explain how does it work?

selectActiveCoaches finishes with return seatsTrainInfo.filter(). This always returns a new array reference, and useSelector will force your component to re-render whenever your selector returns a different reference than last time. So, you are forcing your component to re-render after every dispatched action:
https://react-redux.js.org/api/hooks#equality-comparisons-and-updates
One option here would be to rewrite this as a memoized selector with Reselect:
https://redux.js.org/usage/deriving-data-selectors

Related

nested store function is not a function at runtime (zustand store, immer, persist, typescript)

I've got a problem in a next.js typescript project where i'm using zustand store with persist and immer: https://github.com/pmndrs/zustand
If the toggle_slide_over function is outside of the session_setup object, it works.
If it is inside, it throws runtime error 'not a function'.
Why & how do i fix this?
Interface:
export interface MyAppState {
session_setup: {
slide_over_open: boolean,
toggle_slide_over: (new_toggle_state: boolean) => void,
...
},
session_setup_toggle_slide_over: (new_toggle_state: boolean) => void,
...
}
Function declarations inside doPersist():
const doPersist: () any => { return persist((set) => ({
session_setup: {
...
toggle_slide_over: (new_toggle_state) => set(
produce(state => { state.session_setup.slide_over_open = new_toggle_state })
),
},
session_setup_toggle_slide_over: (new_toggle_state) => set(
produce(state => { state.session_setup.slide_over_open = new_toggle_state })
),
...
How they are retrieved in React:
// When nested:
const toggle_slide_over = useMyAppStore(state => state.session_setup.toggle_slide_over)
// When not nested:
const toggle_slide_over = useMyAppStore(state => state.session_setup_toggle_slide_over)
How they are used:
onClick={() => toggle_slide_over(new_state)}
Store:
const initializeStoreByEnv = (): any => {
if (process.env.NODE_ENV === 'development') return devtools(doPersist())
else return doPersist()
}
export const useMyAppStore = create<MyAppState>()(
initializeStoreByEnv()
)
I've searched for related zustand/immer/redux/functions nested in objects/function not a function at runtime errors but haven't found anything useful, yet...
I'm working around this by just prefixing out-of-object functions with 'session_setup_', which is ok atm, but will get xxxl names with another level of nesting objects.
I completely ran into the same issue. I had nested slices, containing data and functions, which i wanted to persist using zustand/persist. I ended up going with this following approach:
src/zustand/store/index.ts
export const useMyAppStore = create<MyAppState>()(
persist(
(...args) => ({
foo: createFooSlice(...args)
}),
{
name: "app-store",
merge: (persistedState, defaultState) => {
if (!persistedState || typeof persistedState !== "object") {
return defaultState;
}
let resultState: MyAppState = { ...defaultState };
const keys = Object.keys(defaultState) as (keyof MyAppState)[]
keys.forEach((key) => {
if (key in persistedState) {
//#ts-ignore // TypeScript currently don't recognize that key exists in localState
const state = persistedState[key]
if (!!state) {
resultState = { ...resultState, [key]: { ...defaultState[key], ...state }}
}
}
})
return resultState;
}
}
)

How to synchronous useState with passing state to localstorage

I ran into an asynchronous useState problem.
I have a situation where I first need to add an object to the state array in the handler. And then add this state to the localStorage.
setFavoritedSongs ((prev) => [...prev, {name: "Lala", length: "3:20"}]);
localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
If I delete the entire localStorage first and run the handler. So an empty array is added to my localStorage (the state shows me updated). After the action again, the required object is finally added to my array.
I tried something like this, but still the same problem.
const tempArray = favoritedSongs.push({ name: "Lala", length: "3:20" });
localStorage.setItem(storageItemName, JSON.stringify(tempArray));
How do you solve this, please?
/// EDIT
I have something like this
const FavoriteSong = () => {
const song = { id: 1, name: "Lala", length: "3:20" };
const [favoritedSongs, setFavoritedSongs] = useState([]);
const [isFavorited, setIsFavorited] = useState(false);
useEffect(() => {
if (localStorage.getItem("storageItemName")) {
const storageSongs = JSON.parse(
localStorage.getItem("storageItemName") || ""
);
setFavoritedSongs(storageSongs);
const foundSong = storageSongs?.find((song) => song.id === song.id);
foundSong ? setIsFavorited(true) : setIsFavorited(false);
}
}, [song]);
const handleClick = () => {
if (isFavorited) {
const filteredSong = favoritedSongs.filter(
(song) => song.id !== song.id
);
localStorage.setItem("storageItemName", JSON.stringify(filteredSong));
setIsFavorited(false);
} else {
setFavoritedSongs((prev) => [...prev, song]);
localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));
setIsFavorited(true);
}
};
return <div onClick={handleClick}>CLICK</div>;
};
export default FavoriteSong;
Just place your localStorage.set logic inside a useEffect to make sure it runs after the state actually changes.
useEffect() => {
localStorage.setItem(...);
}, [favoritedSongs]};
For that you can Use the condition If data in the array then It will set in localStorage otherwise not
const tempArray = favoritedSongs.push({ name: "Lala", length: "3:20" });
tempArray.length && localStorage.setItem(storageItemName, JSON.stringify(tempArray));
.
setFavoritedSongs ((prev) => [...prev, {name: "Lala", length: "3:20"}]);
FavoritedSongs.length(your state name) && localStorage.setItem("storageItemName", JSON.stringify(favoritedSongs));

update state in useReducer()

first of all I have to mention that i'm a bit new to react but I've spent couple of hours on a problem with useReducer and couldn't understand it . here is my code:
const defaultCartState = {
items: [],
totalAmount: 0,
};
//action = {type:"ADD" , item:item}
const cartReducer = (state, action) => {
if (action.type === "ADD") {
const updatedTotalAmount = parseFloat(
state.totalAmount + action.item.price * action.item.amount
).toFixed(2);
const existingCartItemIndex = state.items.findIndex(
(item) => item.id === action.item.id
);
const existingCartItem = state.items[existingCartItemIndex];
let updatedItems;
if (existingCartItem) {
const updatedItem = state.items[existingCartItemIndex];
updatedItem.amount =
state.items[existingCartItemIndex].amount + action.item.amount;
const updatedItems = [...state.items];
updatedItems[existingCartItemIndex] = updatedItem;
return {
items: updatedItems,
totalAmount: updatedTotalAmount,
};
} else {
const updatedItems = state.items.concat(action.item);
return {
items: updatedItems,
totalAmount: updatedTotalAmount,
};
}
}
return defaultCartState;
};
its simply my reducer function state include an array named items which each element of this array is an object with a amount value. and here is my useReducer initialization:
const [cartState, dispatchCartAction] = useReducer(cartReducer,defaultCartState);
my problem is with these two lines of code :
if (existingCartItem) {
const updatedItem = state.items[existingCartItemIndex];
updatedItem.amount =
state.items[existingCartItemIndex].amount + action.item.amount;
if I swap these two lines of code with these everything works fine .
if (existingCartItem) {
const updatedItem = {
...existingCartItem,
amount: existingCartItem.amount + action.item.amount,
};
I want to know what is the problem ? why my approach doesn't work? what is the difference between defining an item like my solution and the real solution ?
thank you in advance
As reducers are pure functions you cannot mutate state directly. That is why the second method is working. You need to copy the previous state and then and create a new object every time.
I think this may help more (if I understood the question correctly);

Too many re-renders on setting state

I have the following code which is causing too many renders.
const passAcrossSelectedGame = props => {
if (props.passedGamesFlag === 1) {
props.setPassedGamesFlag(0);
gameDetails = {
blackKingSquare: '',
whiteKingSquare: '',
};
plyViewed = 0;
setHistory(game.history());
const auxGame = new Game();
gameHistory = [];
gameHistory.push(auxGame.fen());
game.history().forEach(move => {
auxGame.move(move);
fenHistory.push(auxGame.fen());
});
}
};
passAcrossSelectedGame(props);
I've identified the offending line as setHistory(game.history());
When I comment out that line, I do not get the constant re-rendering. But I need it in there! What solution might be suitable for this?
You should put your function call modifying the state inside a useEffect hook:
const passAcrossSelectedGame = props => {
if (props.passedGamesFlag === 1) {
props.setPassedGamesFlag(0);
gameDetails = {
blackKingSquare: '',
whiteKingSquare: '',
};
plyViewed = 0;
setHistory(game.history());
const auxGame = new Game();
gameHistory = [];
gameHistory.push(auxGame.fen());
game.history().forEach(move => {
auxGame.move(move);
fenHistory.push(auxGame.fen());
});
}
};
useEffect(() => {
passAcrossSelectedGame(props);
}, [props]
You get this error because setState triggers a rerender, that calls again the setState. Thus creating an infinite loop.

How to get all elements from an atomFamily in recoil?

Im playing around with recoil for the first time and cant figure out how I can read all elements from an atomFamily. Let's say I have an app where a user can add meals:
export const meals = atomFamily({
key: "meals",
default: {}
});
And I can initialize a meal as follows:
const [meal, setMeal] = useRecoilState(meals("bananas"));
const bananas = setMeal({name: "bananas", price: 5});
How can I read all items which have been added to this atomFamily?
You have to track all ids of the atomFamily to get all members of the family.
Keep in mind that this is not really a list, more like a map.
Something like this should get you going.
// atomFamily
const meals = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
When creating a new objects inside the family you also have to update the mealIds atom.
I usually use a useRecoilCallback hook to sync this together
const createMeal = useRecoilCallback(({ set }) => (mealId, price) => {
set(mealIds, currVal => [...currVal, mealId]);
set(meals(mealId), {name: mealId, price});
}, []);
This way you can create a meal by calling:
createMeal("bananas", 5);
And get all ids via:
const ids = useRecoilValue(mealIds);
Instead of using useRecoilCallback you can abstract it with selectorFamily.
// atomFamily
const mealsAtom = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set}, meal) => {
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
Further more, in case you would like to support reset you can use the following code:
// atomFamily
const mealsAtom = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set, reset}, meal) => {
if(meal instanceof DefaultValue) {
// DefaultValue means reset context
reset(mealsAtom(id));
reset(mealIds (id));
return;
}
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
If you're using Typescript you can make it more elegant by using the following guard.
import { DefaultValue } from 'recoil';
export const guardRecoilDefaultValue = (
candidate: unknown
): candidate is DefaultValue => {
if (candidate instanceof DefaultValue) return true;
return false;
};
Using this guard with Typescript will look something like:
// atomFamily
const mealsAtom = atomFamily<IMeal, number>({
key: "meals",
default: {}
});
const mealIds = atom<number[]>({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily<IMeal, number>({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set, reset}, meal) => {
if (guardRecoilDefaultValue(meal)) {
// DefaultValue means reset context
reset(mealsAtom(id));
reset(mealIds (id));
return;
}
// from this line you got IMeal (not IMeal | DefaultValue)
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
You can use an atom to track the ids of each atom in the atomFamily. Then use a selectorFamily or a custom function to update the atom with the list of ids when a new atom is added or deleted from the atomFamily. Then, the atom with the list of ids can be used to extract each of the atoms by their id from the selectorFamily.
// File for managing state
//Atom Family
export const mealsAtom = atomFamily({
key: "meals",
default: {},
});
//Atom ids list
export const mealsIds = atom({
key: "mealsIds",
default: [],
});
This is how the selectorFamily looks like:
// File for managing state
export const mealsSelector = selectorFamily({
key: "mealsSelector",
get: (mealId) => ({get}) => {
return get(meals(mealId));
},
set: (mealId) => ({set, reset}, newMeal) => {
// if 'newMeal' is an instance of Default value,
// the 'set' method will delete the atom from the atomFamily.
if (newMeal instanceof DefaultValue) {
// reset method deletes the atom from atomFamily. Then update ids list.
reset(mealsAtom(mealId));
set(mealsIds, (prevValue) => prevValue.filter((id) => id !== mealId));
} else {
// creates the atom and update the ids list
set(mealsAtom(mealId), newMeal);
set(mealsIds, (prev) => [...prev, mealId]);
}
},
});
Now, how do you use all this?
Create a meal:
In this case i'm using current timestamp as the atom id with Math.random()
// Component to consume state
import {mealsSelector} from "your/path";
import {useSetRecoilState} from "recoil";
const setMeal = useSetRecoilState(mealsSelector(Math.random()));
setMeal({
name: "banana",
price: 5,
});
Delete a meal:
// Component to consume state
import {mealsSelector} from "your/path";
import {DefaultValue, useSetRecoilState} from "recoil";
const setMeal = useSetRecoilState(mealsSelector(mealId));
setMeal(new DefaultValue());
Get all atoms from atomFamily:
Loop the list of ids and render Meals components that receive the id as props and use it to get the state for each atom.
// Component to consume state, parent of Meals component
import {mealsIds} from "your/path";
import {useRecoilValue} from "recoil";
const mealIdsList = useRecoilValue(mealsIds);
//Inside the return function:
return(
{mealIdsList.slice()
.map((mealId) => (
<MealComponent
key={mealId}
id={mealId}
/>
))}
);
// Meal component to consume state
import {mealsSelector} from "your/path";
import {useRecoilValue} from "recoil";
const meal = useRecoilValue(mealsSelector(props.id));
Then, you have a list of components for Meals, each with their own state from the atomFamily.
Here is how I have it working on my current project:
(For context this is a dynamic form created from an array of field option objects. The form values are submitted via a graphql mutation so we only want the minimal set of changes made. The form is therefore built up as the user edits fields)
import { atom, atomFamily, DefaultValue, selectorFamily } from 'recoil';
type PossibleFormValue = string | null | undefined;
export const fieldStateAtom = atomFamily<PossibleFormValue, string>({
key: 'fieldState',
default: undefined,
});
export const fieldIdsAtom = atom<string[]>({
key: 'fieldIds',
default: [],
});
export const fieldStateSelector = selectorFamily<PossibleFormValue, string>({
key: 'fieldStateSelector',
get: (fieldId) => ({ get }) => get(fieldStateAtom(fieldId)),
set: (fieldId) => ({ set, get }, fieldValue) => {
set(fieldStateAtom(fieldId), fieldValue);
const fieldIds = get(fieldIdsAtom);
if (!fieldIds.includes(fieldId)) {
set(fieldIdsAtom, (prev) => [...prev, fieldId]);
}
},
});
export const formStateSelector = selectorFamily<
Record<string, PossibleFormValue>,
string[]
>({
key: 'formStateSelector',
get: (fieldIds) => ({ get }) => {
return fieldIds.reduce<Record<string, PossibleFormValue>>(
(result, fieldId) => {
const fieldValue = get(fieldStateAtom(fieldId));
return {
...result,
[fieldId]: fieldValue,
};
},
{},
);
},
set: (fieldIds) => ({ get, set, reset }, newValue) => {
if (newValue instanceof DefaultValue) {
reset(fieldIdsAtom);
const fieldIds = get(fieldIdsAtom);
fieldIds.forEach((fieldId) => reset(fieldStateAtom(fieldId)));
} else {
set(fieldIdsAtom, Object.keys(newValue));
fieldIds.forEach((fieldId) => {
set(fieldStateAtom(fieldId), newValue[fieldId]);
});
}
},
});
The atoms are selectors are used in 3 places in the app:
In the field component:
...
const localValue = useRecoilValue(fieldStateAtom(fieldId));
const setFieldValue = useSetRecoilState(fieldStateSelector(fieldId));
...
In the save-handling component (although this could be simpler in a form with an explicit submit button):
...
const fieldIds = useRecoilValue(fieldIdsAtom);
const formState = useRecoilValue(formStateSelector(fieldIds));
...
And in another component that handles form actions, including form reset:
...
const resetFormState = useResetRecoilState(formStateSelector([]));
...
const handleDiscard = React.useCallback(() => {
...
resetFormState();
...
}, [..., resetFormState]);

Resources