custom hook create infinite loop - reactjs

This hook create an inifite loop. I don't understnd why, since my dependencies array is set.
Error : Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Custom hook :
export const useListGuessers = () => {
const [list, setList] = useState([]);
const hasMarketing = UserHelper.hasAuthorization(AUTHORIZATION_MARKETING);
const hasTechnical = UserHelper.hasAuthorization(AUTHORIZATION_TECHNICAL);
const dashboardGroups = new DashboardGroups({hasMarketing, hasTechnical});
const guessers = [
...dashboardGroups.appProductGroup(),
...dashboardGroups.articlesGroup(),
...dashboardGroups.mediasGroup(),
...dashboardGroups.productsGroup(),
...dashboardGroups.orderableProductsGroup(),
...dashboardGroups.typesGroup(),
...dashboardGroups.usersGroup(),
...dashboardGroups.othersGroup(),
...dashboardGroups.userManagementGroup(),
];
const filteredGuesser = guessers
.filter(({canShow}) => canShow)
.map((guesser: any) => {
return {
label: guesser.label ?? guesser.value.options.label,
link: (guesser.operation && `user-management/${guesser.operation}`) ?? guesser.value.name,
};
})
.sort((a, b) => a.label.localeCompare(b.label));
useEffect(() => {
filteredGuesser && setList(filteredGuesser);
}, [filteredGuesser]);
return list;
};
The class :
export class DashboardGroups {
authorizations: {hasMarketing: boolean; hasTechnical: boolean};
constructor(authorizations: {hasMarketing: boolean; hasTechnical: boolean}) {
this.authorizations = authorizations;
}
// [all groups comes here...]
getGroups = () => {
// return an object for each groups with labels, and the group as "children"
};
}

Since filteredGuesser calculates on each re-render, which triggers useEffect(..., [filterGuesser] which causes re-render... so it loops.
The easiest straighforward solution is to ensure reference equality for filteredGuesser with useMemo. Then it will be referentially the same until guessers is changed:
const filteredGuesser = useMemo(() =>
guessers
.filter(({canShow}) => canShow)
.map((guesser: any) => {
label: guesser.label ?? guesser.value.options.label,
link: (guesser.operation && `user-management/${guesser.operation}`) ?? guesser.value.name,
})
.sort((a, b) => a.label.localeCompare(b.label))
, [guessers]);
However, I think the better solution would be reconsider need in
useEffect(() =>
...
setList(filteredGuesser)
This storing ready for use calculation into state does not seem reasonable to me. I think you better use filteredGuesser directly, instead of storing it into list state
Beta docs for useMemo
Referential equality aka strict equality on MDN

Related

React useState is not re-rendering while it is referring cloned array by spreading

I am trying to sort an array and reflect its sort result immediately by useState hook.
I already new that react is detecting its state change by Object.is(), so trying to spread array before using useState like below;
const [reviews, setReviews] = useState([...book.reviews]);
useEffect(() => {
const sortedReviews = [...reviews]
.sort((review1: IReview, review2: IReview) =>
sortBy(review1, review2, sortRule));
setReviews([...sortedReviews])
}, [sortRule])
After sorting I checked the value of variable sortedReviews and it was sorted as expected, but react did not re-render the page so this sorting was not reflected to UI.
I already searched solutions and it seemed many could solve the issue by spreading an array to before calling useState like this stack overflow is explaining: Why is useState not triggering re-render?.
However, on my end it is not working.. Any help will be very appreciated. Thank you!
[Added]
And my rendering part is like below;
<>
{
sortedReviews
.map((review: IReview) => (
<ReviewBlock id={review.id}
review={review}
targetBook={book}
setTargetBook={setBook}/>
))
}
</>
Sometimes I facing this issue too, in my case I just "force" render calling callback.
First solution:
const [reviews, setReviews] = useState([...book.reviews]);
useEffect(() => {
const sortedReviews = [...reviews]
.sort((review1: IReview, review2: IReview) =>
sortBy(review1, review2, sortRule));
setReviews(()=> [...sortedReviews]) // <-- Here
}, [sortRule])
second solution:
You can use useRef to get data in real time, see below:
const [reviews, setReviews] = useState([...book.reviews]);
const reviewsRef = useRef();
useEffect(() => {
const sortedReviews = [...reviews]
.sort((review1: IReview, review2: IReview) =>
sortBy(review1, review2, sortRule));
setReviewRef([...sortedReviews])
}, [sortRule])
function setReviewRef(data){
setReview(data);
reviewsRef.current = data;
}
So, instead use the state reviews use reviewsRef.current as u array
I hope you can solve this!
When components don't re-render it is almost always due to mutations of state. Here you are calling a mutating operation .sort on reviews. You would need to spread the array before you mutate it.
useEffect(() => {
const sortedReviews = [...reviews].sort((review1: IReview, review2: IReview) =>
sortBy(review1, review2, sortRule)
);
setReviews(sortedReviews);
}, [sortRule]);
But there are other issues here. reviews is not a dependency of the useEffect so we would want to use a setReviews callback like setReviews(current => [...current].sort(....
In general sorted data makes more sense as a useMemo than a useState. The sortRule is a state and the sortedReviews are derived from it.
The way that you are calling sortBy as a comparer function feels a bit off. Maybe it's just a confusing name?
Also you should not need to include the type IReview in your callback if book is typed correctly as {reviews: IReview[]}.
If you include your sortBy function and sortRule variable then I can be of more help. But here's what I came up with.
import React, { useState, useMemo } from "react";
type IReview = {
rating: number;
}
type Book = {
reviews: IReview[]
}
type SortRule = string; // just a placeholder - what is this really?
declare function sortBy(a: IReview, b: IReview, rule: SortRule): number;
const MyComponent = ({book}: {book: Book}) => {
const [sortRule, setSortRule] = useState("");
const reviews = book.reviews;
const sortedReviews = useMemo( () => {
return [...reviews].sort((review1, review2) =>
sortBy(review1, review2, sortRule)
);
}, [sortRule, reviews]);
...
}
Typescript Playground Link

Limit renders of component that uses useContext via a useFooController/useFooHook

I'm using custom hooks for a component, and the custom hook uses a custom context. Consider
/* assume FooContext has { state: FooState, dispatch: () => any } */
const useFoo = () => {
const { state, dispatch } = useContext(FooContextContext)
return {apiCallable : () => apiCall(state) }
}
const Foo = () => {
const { apiCallable } = useFoo()
return (
<Button onClick={apiCallable}/>
)
}
Lots of components will be making changes to FooState from other components (form inputs, etc.). It looks to me like Foo uses useFoo, which uses state from FooStateContext. Does this mean every change to FooContext will re-render the Foo component? It only needs to make use of state when someone clicks the button but never otherwise. Seems wasteful.
I was thinking useCallback is specifically for this, so I am thinking return {apiCallable : useCallback(() => apiCall(state)) } but then I need to add [state] as a second param of useCallback. Then that means the callback will be re-rendered whenever state updates, so I'm back at the same issue, right?
This is my first time doing custom hooks like this. Having real difficulty understanding useCallback. How do I accomplish what I want?
Edit Put another way, I have lots of components that will dispatch small changes to deeply nested properties of this state, but this particular component must send the entire state object via a RESTful API, but otherwise will never use the state. It's irrelevant for rendering this component completely. I want to make it so this component never renders even when I'm making changes constantly to the state via keypresses on inputs (for example).
Since you provided Typescript types in your question, I will use them in my response.
Way One: Split Your Context
Given a context of the following type:
type ItemContext = {
items: Item[];
addItem: (item: Item) => void;
removeItem: (index: number) => void;
}
You could split the context into two separate contexts with the following types:
type ItemContext = Item[];
type ItemActionContext = {
addItem: (item: Item) => void;
removeItem: (index: number) => void;
}
The providing component would then handle the interaction between these two contexts:
const ItemContextProvider = () => {
const [items, setItems] = useState([]);
const actions = useMemo(() => {
return {
addItem: (item: Item) => {
setItems(currentItems => [...currentItems, item]);
},
removeItem: (index: number) => {
setItems(currentItems => currentItems.filter((item, i) => index === i));
}
};
}, [setItems]);
return (
<ItemActionContext.Provider value={actions}>
<ItemContext.Provider value={items}>
{children}
</ItemContext.Provider>
</ItemActionContext.Provider>
)
};
This would allow you to get access to two different contexts that are part of one larger combined context.
The base ItemContext would update as items are added and removed causing rerenders for anything that was consuming it.
The assoicated ItemActionContext would never update (setState functions do not change for their lifetime) and would never directly cause a rerender for a consuming component.
Way Two: Some Version of an Subscription Based Value
If you make the value of your context never change (mutate instead of replace, HAS THE WORLD GONE CRAZY?!) you can set up a simple object that holds the data you need access to and minimises rerenders, kind of like a poor mans Redux (maybe it's just time to use Redux?).
If you make a class similar to the following:
type Subscription<T> = (val: T) => void;
type Unsubscribe = () => void;
class SubscribableValue<T> {
private subscriptions: Subscription<T>[] = [];
private value: T;
constructor(val: T) {
this.value = val;
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.subscribe = this.subscribe.bind(this);
}
public get(): T {
return this._val;
}
public set(val: T) {
if (this.value !== val) {
this.value = val;
this.subscriptions.forEach(s => {
s(val)
});
}
}
public subscribe(subscription: Subscription<T>): Unsubscriber {
this.subscriptions.push(subscription);
return () => {
this.subscriptions = this.subscriptions.filter(s => s !== subscription);
};
}
}
A context of the following type could then be created:
type ItemContext = SubscribableValue<Item[]>;
The providing component would look something similar to:
const ItemContextProvider = () => {
const subscribableValue = useMemo(() => new SubscribableValue<Item[]>([]), []);
return (
<ItemContext.Provider value={subscribableValue}>
{children}
</ItemContext.Provider>
)
};
You could then use some a custom hooks to access the value as needed:
// Get access to actions to add or remove an item.
const useItemContextActions = () => {
const subscribableValue = useContext(ItemContext);
const addItem = (item: Item) => subscribableValue.set([...subscribableValue.get(), item]);
const removeItem = (index: number) => subscribableValue.set(subscribableValue.get().filter((item, i) => i === index));
return {
addItem,
removeItem
}
}
type Selector = (items: Item[]) => any;
// get access to data stored in the subscribable value.
// can provide a selector which will check if the value has change each "set"
// action before updating the state.
const useItemContextValue = (selector: Selector) => {
const subscribableValue = useContext(ItemContext);
const selectorRef = useRef(selector ?? (items: Item[]) => items)
const [value, setValue] = useState(selectorRef.current(subscribableValue.get()));
const useEffect(() => {
const unsubscribe = subscribableValue.subscribe(items => {
const newValue = selectorRef.current(items);
if (newValue !== value) {
setValue(newValue);
}
})
return () => {
unsubscribe();
};
}, [value, selectorRef, setValue]);
return value;
}
This would allow you to reduce rerenders using selector functions (like an extremely basic version of React Redux's useSelector) as the subscribable value (root object) would never change reference for its lifetime.
The downside of this is that you have to manage the subscriptions and always use the set function to update the held value to ensure that the subscriptions will be notified.
Conclusion:
There are probably a number of other ways that different people would attack this problem and you will have to find one that suits your exact issue.
There are third party libraries (like Redux) that could also help you with this if your context / state requirements have a larger scope.
Does this mean every change to FooContext will re-render the Foo component?
Currently (v17), there is no bailout for Context API. Check my another answer for examples. So yes, it will always rerender on context change.
It only needs to make use of state when someone clicks the button but never otherwise. Seems wasteful.
Can be fixed by splitting context providers, see the same answer above for explanation.

Why doesn't a useEffect hook trigger with an object in the dependency array?

I'm noticing something really strange while working with hooks, I've got the following:
import React, { useState, useEffect } from "react";
const [dependency1, setDependency1] = useState({});
const [dependency2, setDependency2] = useState([]);
useEffect(() => {
console.log("dependency 1 got an update");
}, [dependency1]);
useEffect(() => {
console.log("dependency 2 got an update");
}, [dependency2]);
setInterval(() => {
setDependency1(prevDep1 => {
const _key = "test_" + Math.random().toString();
if (prevDep1[_key] === undefined) prevDep1[_key] = [];
else prevDep1[key].push("foo");
return prevDep1;
})
setDependency2(prevDep2 => [...prevDep2, Math.random()]);
}, 1000);
for some reason only the useEffect with dependency2 (the array where items get added) triggers, the one with dependency1 (the object where keys get added) doesn't trigger..
Why is this happening, and how can I make it work?
setInterval(() => {
setDependency1(prevDep1 => {
const _key = "test_" + Math.random().toString();
return {...prevDep1, [_key]: [...(prevDep1[_key] || []), 'foo'] }
})
setDependency2(prevDep2 => [...prevDep2, Math.random()]);
}, 1000);
State should be updated in an immutable way.
React will only check for reference equality when deciding a dependency changed, so if the old and new values pass a === check, it considers it unchanged.
In your first dependency you simply added a key to the existing object, thus not changing the actual object. The second dependency actually gets replaced altogether when spreading the old values into a new array.
You're returning an assignment statement here:
setDependency1(prevDep1 => prevDep1["test_" + Math.random().toString()] = ["foo"]);
You should return an object. Maybe something like:
setDependency1(prevDep1 => ({ ...prevDep1, ["test_" + Math.random().toString()]: ["foo"] }));

Passing array to useEffect dependency list

There's some data coming from long polling every 5 seconds and I would like my component to dispatch an action every time one item of an array (or the array length itself) changes.
How do I prevent useEffect from getting into infinity loop when passing an array as dependency to useEffect but still manage to dispatch some action if any value changes?
useEffect(() => {
console.log(outcomes)
}, [outcomes])
where outcomes is an array of IDs, like [123, 234, 3212]. The items in array might be replaced or deleted, so the total length of the array might - but don't have to - stay the same, so passing outcomes.length as dependency is not the case.
outcomes comes from reselect's custom selector:
const getOutcomes = createSelector(
someData,
data => data.map(({ outcomeId }) => outcomeId)
)
You can pass JSON.stringify(outcomes) as the dependency list:
Read more here
useEffect(() => {
console.log(outcomes)
}, [JSON.stringify(outcomes)])
Using JSON.stringify() or any deep comparison methods may be inefficient, if you know ahead the shape of the object, you can write your own effect hook that triggers the callback based on the result of your custom equality function.
useEffect works by checking if each value in the dependency array is the same instance with the one in the previous render and executes the callback if one of them is not. So we just need to keep the instance of the data we're interested in using useRef and only assign a new one if the custom equality check return false to trigger the effect.
function arrayEqual(a1: any[], a2: any[]) {
if (a1.length !== a2.length) return false;
for (let i = 0; i < a1.length; i++) {
if (a1[i] !== a2[i]) {
return false;
}
}
return true;
}
type MaybeCleanUpFn = void | (() => void);
function useNumberArrayEffect(cb: () => MaybeCleanUpFn, deps: number[]) {
const ref = useRef<number[]>(deps);
if (!arrayEqual(deps, ref.current)) {
ref.current = deps;
}
useEffect(cb, [ref.current]);
}
Usage
function Child({ arr }: { arr: number[] }) {
useNumberArrayEffect(() => {
console.log("run effect", JSON.stringify(arr));
}, arr);
return <pre>{JSON.stringify(arr)}</pre>;
}
Taking one step further, we can also reuse the hook by creating an effect hook that accepts a custom equality function.
type MaybeCleanUpFn = void | (() => void);
type EqualityFn = (a: DependencyList, b: DependencyList) => boolean;
function useCustomEffect(
cb: () => MaybeCleanUpFn,
deps: DependencyList,
equal?: EqualityFn
) {
const ref = useRef<DependencyList>(deps);
if (!equal || !equal(deps, ref.current)) {
ref.current = deps;
}
useEffect(cb, [ref.current]);
}
Usage
useCustomEffect(
() => {
console.log("run custom effect", JSON.stringify(arr));
},
[arr],
(a, b) => arrayEqual(a[0], b[0])
);
Live Demo
Another ES6 option would be to use template literals to make it a string. Similar to JSON.stringify(), except the result won't be wrapped in []
useEffect(() => {
console.log(outcomes)
}, [`${outcomes}`])
Another option, if the array size doesn't change, would be to spread it in:
useEffect(() => {
console.log(outcomes)
}, [ ...outcomes ])
As an addendum to loi-nguyen-huynh's answer, for anyone encountering the eslint exhaustive-deps warning, this can be resolved by first breaking the stringified JSON out into a variable:
const depsString = JSON.stringify(deps);
React.useEffect(() => {
...
}, [depsString]);
I would recommend looking into this OSS package which was created to address the exact issue you describe (deeply comparing the values in the dependency array instead of shallow):
https://github.com/kentcdodds/use-deep-compare-effect
The usage/API is exactly the same as useEffect but it will compare deeply.
I would caution you however to not use it where you don't need it because it has the potential to result in a performance degredation due to unnecessary deep comparisons where a shallow one would do.
Quick solution, though its kinda of a hack:
const [string, setString] = useState('1');
useEffect(() => {
console.log(outcomes)
}, [string])
And when you update the array 'outcomes' also update the string like this
setString(prev => `${prev}2`)

Should I use useMemo in hooks?

I created useBanner hooks
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState(array.slice(0, yardage));
const [bannListIndex, setBannIndex] = useState(1);
return {
....
};
};
Am I doing the right thing and the props throw in useState.
It’s permissible to use useBanner.
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
when props will change here.
Will change the state in useBanner.
or is it considered anti-patterns I have to write all this in useMemo
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState([]);
const [bannListIndex, setBannIndex] = useState(1);
useMemo(() => {
setBannArr(array.slice(0, yardage));
setBannIndex(1);
}, [array, yardage]);
return {
....
};
};
Yes, custom hooks are possible in React. Here is separate document discussing custom hooks.
But exactly you sample may require additional code depending on what is your final goal.
If you want initialize state only once, when component Banner is first created, you can just do as in your first sample
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
This will work perfectly. But if props array and yardage will change, this will not be reflected in component. So props will be used only once as initial values and then will not be used in useBanner even if changed (And it doesn't matter whether you'll use useBanner or useState directly). This answer highlight this.
If you want to update inital values on each props change, you can go with useEffect like below
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr, setBannArr } = useBanner(array, yardage);
useEffect (() => {
// setBannArr should also be returned from useBanner. Or bannArr should be changed with any other suitable function returned from useBanner.
setBannArr(array.slice(0, yardage));
}, [array, yardage, setBannArr])
return (
...
);
};
In this case Banner component can control state itself and when parent component change props, state in Banner component will be reset to new props.
Here is small sample to showcase second option.

Resources