How do I update state but not trigger the infinite useEffect loop? - javascript

I'm trying to update state in a higher-order component from a child. My solution is to set state to true after setting tags. useEffect runs when state is true, firstly updating state in the parent component and then updating state to false, which halts its invocation. My aforementioned solution is the only way I've managed to prevent useEffect's infinite loop.
const Student = ({
appendTags,
student: {
id: studentId,
firstName,
lastName,
pic,
email,
company,
skill,
grades,
tags: studentTags
}}) => {
const fullName = `${firstName} ${lastName}`;
const [tag, setTag] = useState('');
const [tags, setTags] = useState([]);
const [stateUpdated, setStateUpdated] = useState(false);
const [displayGrades, setDisplayGrades] = useState(false);
const onTagsSubmit = e => {
if (tag.length) {
e.preventDefault();
setTags(prevState => [...prevState, tag]);
setStateUpdated(true);
setTag('');
}
}
useEffect(() => {
if (stateUpdated) {
appendTags(studentId, tags);
setStateUpdated(false);
};
}, [stateUpdated, setStateUpdated, tags, appendTags, studentId]);

Looks like this is what we have, if you remove stateUpdated.
I presume than on appendTags() call the parent component changes its state and gets re-rendered. After that the appendTags function is recreated. The child component Student is recreated, too. Student's useEffect sees that one of the dependencies, appendTags, has changed, so it has to be re-executed. It calls the appendTags() and we have a loop.
To fix it, you need to wrap appendTags into useCallback hook inside the parent component:
const appendTags = useCallback((id, tags) => {
// update local state
}, []);
// ...
return <Student appendTags={appendTags} /* (...) */ />

Related

Props defined by async function by Parent in UseEffect passed to a child component don't persist during its UseEffect's clean-up

Please consider the following code:
Parent:
const Messages = (props) => {
const [targetUserId, setTargetUserId] = useState(null);
const [currentChat, setCurrentChat] = useState(null);
useEffect(() => {
const { userId } = props;
const initiateChat = async (targetUser) => {
const chatroom = `${
userId < targetUser
? `${userId}_${targetUser}`
: `${targetUser}_${userId}`
}`;
const chatsRef = doc(database, 'chats', chatroom);
const docSnap = await getDoc(chatsRef);
if (docSnap.exists()) {
setCurrentChat(chatroom);
} else {
await setDoc(chatsRef, { empty: true });
}
};
if (props.location.targetUser) {
initiateChat(props.location.targetUser.userId);
setTargetUserId(props.location.targetUser.userId);
}
}, [props]);
return (
...
<Chat currentChat={currentChat} />
...
);
};
Child:
const Chat = (props) => {
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, []);
...
The issue I'm dealing with is that Child's UseEffect clean up function, which depends on the chatroom prop passed from its parent, throws a TypeError error because apparently chatroom is null. Namely, it becomes null when the parent component unmounts, the component works just fine while it's mounted and props are recognized properly.
I've tried different approaches to fix this. The only way I could make this work if when I moved child component's useEffect into the parent component and defined currentChat using useRef() which honestly isn't ideal.
Why is this happening? Shouldn't useEffect clean-up function depend on previous state? Is there a proper way to fix this?
currentChat is a dependency of that effect. If it's null, the the unsubscribe should just early return.
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
if(!currentChat) return;
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, [currentChat]);
But that doesn't smell like the best solution. I think you should handle all the subscribing/unsubscribing in the same component. You shouldn't subscribe in the parent and then unsubscribe in the child.
EDIT:
Ah, there's a bunch of stuff going on here that's not good. You've got your userId coming in from props - props.location.targetUser.userId and then you're setting it as state. It's NOT state, it's only a prop. State is something a component owns, some data that a component has created, some data that emanates from that component, that component is it's source of truth (you get the idea). If your component didn't create it (like userId which is coming in on props via the location.targetUser object) then it's not state. Trying to keep the prop in sync with state and worry about all the edge cases is a fruitless exercise. It's just not state.
Also, it's a codesmell to have [props] as a dependency of an effect. You should split out the pieces of props that that effect actually needs to detect changes in and put them in the dependency array individually.

Maximum depth exceeded while using useEffect

I am trying to implement a simple search algorithm for my products CRUD.
The way I thought to do it was entering the input in a search bar, and the products that matched the search would appear instantly every time the user changes the input, without needing to hit a search button.
However, the way I tried to do it was like this:
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
const queryProducts = filterProducts(queryString, productList);
setProductList(queryProducts);
}, [queryString, productList]);
I know that productList changes every render, and that's probably why it isn't working. But I didn't figure out how can I solve the problem. I've seen other problems here and solutions with useReducer, but I none of them seemed to help me.
The error is this one below:
Warning: 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.
what you are doing here is fetching a product list and filtering it based on the query string and using that filtered list to render the UI. So ideally your filteredList is just a derived state based on your queryString and productList. So you can remove the filterProducts from your useEffect and move it outside. So that it runs when ever there is a change in the state.
function filterProducts (productName = '', productList = []) {
return productName.trim().length > 0 ? productList.filter((prod)=> {
return prod.title === productName;
}); : productList
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [queryString, setQueryString] = useState('');
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
}
}, [queryString]);
// query products is the derived state
const queryProducts = filterProducts(queryString, productList);
// Now instead of using productList to render something use the queryProducts
return (
{queryProducts.map(() => {
.....
})}
)
If you want the filterProducts to run only on change in queryString or productList then you can wrap it in useMemo
const queryProducts = React.useMemo(() => filterProducts(queryString, productList), [queryString, productList]);
When you use a setState function in a useEffect hook while having the state for that setState function as one of the useEffect hook's dependencies, you'll get this recursive effect where you end up infinitely re-rendering your component.
So, first of all we have to remove productList from the useEffect. Then, we can use a function to update your state instead of a stale update (like what you're doing in your example).
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
setProductList(prevProductList => {
return filterProducts(queryString, prevProductList)
});
}, [queryString]);
Now, you still get access to productList for your filter, but you won't have to include it in your dependencies, which should take care of the infinite re-rendering.
I recommend several code changes.
I would separate the state that immediately reflects the user input at all times from the state that represents the query that is send to the backend. And I would add a debounce between the two states. Something like this:
const [query, setQuery] = useState('');
const [userInput, setUserInput] = useState('');
useDebounce(userInput, setQuery, 750);
I would split up the raw data that was returned from the backend and the filtered data which is just derived from it
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
I would split up the useEffect and not mix different concerns all into one (there is no rule that you cannot have multiple useEffect)
useEffect(() => {
if (query.trim() === '') {
Axios
.get("http://localhost:3001/api/product/get-all")
.then((data) => { setProducts(data.data) });
}
}, [query]);
useEffect(
() => setFilteredProducts(filterProducts(userInput, products)),
[userInput, products]
);

How to make dispatch call synchronous in useeffect?

i am trying to paginate the data from my rest server using CoreUI Table Component.
i have problem getting the updated data from redux store after dispatch request in useEffect, i am using redux thunk, i know that dispatch is async, but is there a way to wait for the dispatch to be completed? i tired making the dispatch a Promise but it did not work.
I successfully get the updated result from action and reducer but in ProductsTable its the previous one, i checked redux devtools extension and i can see the state being changed.
i never get the latest value from store.
Also the dispatch is being called so many times i can see in the console window, it nots an infinite loop, it stops after sometime.
const ProductsTable = (props) => {
const store = useSelector((state) => state.store);
const dispatch = useDispatch();
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [pages, setPages] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0);
useEffect(() => {
setLoading(true);
const payload = {
params: {
page,
},
};
if (page !== 0)
dispatch(getAllProducts(payload));
console.log("runs:" + page)
console.log(store.objects)
if(!(Object.keys(store.objects).length === 0)){
setItems(store.objects.results)
setPages(store.objects.total.totalPages)
setLoading(false)
} else{
console.log("error")
setFetchTrigger(fetchTrigger + 1);
}
}, [page, fetchTrigger]);
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={["title", "slug", {
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false
}]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
<CPagination
pages={pages}
activePage={page}
onActivePageChange={setPage}
className={pages < 2 ? "d-none" : ""}
/>
</CCard>
)
}
export default ProductsTable
The reason the ProductsTable always has the previous state data is because the effect you use to update the ProductsTable is missing the store as dependency or more specifically store.objects.results; when the page and the fetchTrigger change the effect becomes stale because it isn't aware that when those dependencies change the effect should change.
useEffect(() => {
// store.objects is a dependency that is not tracked
if (!(Object.keys(store.objects).length === 0)) {
// store.objects.results is a dependency that is not tracked
setItems(store.objects.results);
// store.objects.total.totalPages is a dependency that is not tracked
setPages(store.objects.total.totalPages);
setLoading(false);
}
// add these dependencies to the effect so that everything works as expected
// avoid stale closures
}, [page, fetchTrigger, store.objects, store.objects.results, store.objects.total.totalPages]);
The dispatch is being called many times because you have a recursive case where fetchTrigger is a dependency of the effect but you also update it from within the effect. By removing that dependency you'll see much less calls to this effect, namely only when the page changes. I don't know what you need that value for because I dont see it used in the code you've shared, but if you do need it I recommend using the callback version of setState so that you can reference the value of fetchTrigger that you need without needing to add it as a dependency.
useEffect(() => {
// code
if (!(Object.keys(store.objects).length === 0)) {
// code stuffs
} else {
// use the callback version of setState to get the previous/current value of fetchTrigger
// so you can remove the dependency on the fetchTrigger
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
}
// remove fetchTrigger as a dependency
}, [page, store.objects, store.objects.results, store.objects.totalPages]);
With those issues explained, you'd be better off not adding new state for your items, pages, or loading and instead deriving that from your redux store, because it looks like thats all it is.
const items = useSelector((state) => state.store.objects?.results);
const pages = useSelector((state) => state.store.objects?.total?.totalPages);
const loading = useSelector((state) => !Object.keys(state.store.objects).length === 0);
and removing the effect entirely in favor of a function to add to the onActivePageChange event.
const onActivePageChange = page => {
setPage(page);
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
dispatch(getAllProducts({
params: {
page,
},
}));
};
return (
<CPagination
// other fields
onActivePageChange={onActivePageChange}
/>
);
But for initial results you will still need some way to fetch, you can do this with an effect that only runs once when the component is mounted. This should do that because dispatch should not be changing.
// on mount lets get the initial results
useEffect(() => {
dispatch(
getAllProducts({
params: {
page: 1,
},
})
);
},[dispatch]);
Together that would look like this with the recommended changes:
const ProductsTable = props => {
const items = useSelector(state => state.store.objects?.results);
const pages = useSelector(state => state.store.objects?.total?.totalPages);
const loading = useSelector(state => !Object.keys(state.store.objects).length === 0);
const dispatch = useDispatch();
const [page, setPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0);
// on mount lets get the initial results
useEffect(() => {
dispatch(
getAllProducts({
params: {
page: 1,
},
})
);
},[dispatch]);
const onActivePageChange = page => {
setPage(page);
setFetchTrigger(fetchTrigger => fetchTrigger + 1);
dispatch(
getAllProducts({
params: {
page,
},
})
);
};
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={[
'title',
'slug',
{
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false,
},
]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
/>
<CPagination
pages={pages}
activePage={page}
onActivePageChange={onActivePageChange}
className={pages < 2 ? 'd-none' : ''}
/>
</CCard>
);
};
export default ProductsTable;
first of all you don't need any synchronous to achieve the results you want, you have to switch up your code so it doesn't use the state of react since you are already using some kind of global store ( i assume redux ); what you need to do is grab all the items straight from the store don't do an extra logic on the component (read for the separation of concerns); Also I would suggest to do the pagination on the server side not just paginate data on the front end. (getAllProducts() method to switch on fetching just a page of results and not all the products); Your code have alot of dispatches because you are using page and fetchTrigger as dependencies of useEffect hook that means every time the page or fetchTrigger value changes the code inside useEffect will run again resulting in another dispatch;
Here is a slightly modified part of your code, you need to add some extra stuff on your action and a loading param in your global state
const ProductsTable = (props) => {
const dispatch = useDispatch();
// PUT THE LOADING IN THE GLOBAL STATE OR HANDLE IT VIA A CALLBACK OR ADD A GLOBAL MECHANISM TO HANLE LOADINGS INSIDE THE APP
const loading = useSelector(() => state.store.loading)
const items = useSelector((state) => state.store.objects?.results); // ADD DEFAULT EMPTY VALUES FOR objects smthg like : { objects: { results: [], total: { totalPages: 0 } }}
const pages = useSelector((state) => state.store.objects?.total?.totalPages);
const [page, setPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [fetchTrigger, setFetchTrigger] = useState(0); // I DONT UNDERSTAND THIS ONE
const fetchData = () => dispatch(getAllProducts({ params: { page }}));
useEffect(() => {
fetchData();
}, []);
return (
<CCard className="p-5">
<CDataTable
items={items}
fields={["title", "slug", {
key: 'show_details',
label: '',
_style: { width: '1%' },
sorter: false,
filter: false
}]}
loading={loading}
hover
cleaner
sorter
itemsPerPage={itemsPerPage}
onPaginationChange={setItemsPerPage}
<CPagination
pages={pages}
activePage={page}
onActivePageChange={setPage}
className={pages < 2 ? "d-none" : ""}
/>
</CCard>
)
}
export default ProductsTable

How to use a prop function inside of UseEffect?

I want to call a prop function inside of UseEffect. The following code works:
useEffect(() => {
props.handleClick(id);
}, [id]);
But lint is complaining about props not being a dependency.
If I do this, then the code no longer works and I have maximum re-render error:
useEffect(() => {
props.handleClick(id);
}, [id, props]);
How can I fix the lint issue?
Sample code:
Parent Component
const ParentGrid = ({rows, columns}) => {
const [selection, setSelection] = useState(null);
const handleClick = selectedRows => {
setSelection(selectedRows.map(i => rows[i]));
};
return (
<ChildGrid
columns={columns}
data={data}
handleClick={handleClick}
/>
Child Component
const ChildGrid = props => {
const {data, handleClick, ...rest} = props;
useEffect(() => {
handleClick(selectedRows);
}, [selectedRows]);
I see alot of weird and incorrect answers here so I felt the need to write this.
The reason you are reaching maximum call depth when you add the function to your dependency array is because it does not have a stable identity. In react, functions are recreated on every render if you do not wrap it in a useCallback hook. This will cause react to see your function as changed on every render, thus calling your useEffect function every time.
One solution is to wrap the function in useCallback where it is defined in the parent component and add it to the dependency array in the child component. Using your example this would be:
const ParentGrid = ({rows, columns}) => {
const [selection, setSelection] = useState(null);
const handleClick = useCallback((selectedRows) => {
setSelection(selectedRows.map(i => rows[i]));
}, [rows]); // setSelection not needed because react guarantees it's stable
return (
<ChildGrid
columns={columns}
data={data}
handleClick={handleClick}
/>
);
}
const ChildGrid = props => {
const {data, handleClick, ...rest} = props;
useEffect(() => {
handleClick(selectedRows);
}, [selectedRows, handleClick]);
};
(This assumes the rows props in parent component does not change on every render)
One correct way to do this is to add props.handleClick as a dependency and memoize handleClick on the parent (useCallback) so that the function reference does not change unnecessarily between re-renders.
It is generally NOT advisable to switch off the lint rule as it can help with subtle bugs (current and future)
in your case, if you exclude handleClick from deps array, and on the parent the function was dependent on parent prop or state, your useEffect will not fire when that prop or state on the parent changes, although it should, because the handleClick function has now changed.
you should destructure handleClick outside of props
at the start of the component you probably have something like this:
const myComponent = (props) =>
change to
const myComponent = ({ handleClick, id }) basically you can pull out any props you know as their actual name
then use below like so:
useEffect(() => {
handleClick(id);
}, [id, handleClick]);
or probably you don't actually need the function as a dependency so this should work
useEffect(() => {
handleClick(id);
}, [id]);
Add props.handleClick as the dependency
useEffect(() => {
props.handleClick(id);
}, [id, props.handleClick]);

React hooks error: Rendered more hooks than during the previous render

I used to have a function component act as a page:
export default function NormalList(props) {
const pageSize = 20;
const [page, setPage] = useState(1)
const [searchString, setSearchString] = useState(null);
const [creditNotes, setCreditNotes] = useState(() => getCreditNoteList());
const [ordering, setOrdering] = useState(null);
useEffect(() => getCreditNoteList(), [page, searchString, ordering]);
function getCreditNoteList() {
API.fetchCreditNoteList({
account_id: props.customerId, page, page_size: pageSize, searchString, ordering
}).then(data => {
setCreditNotes(data);
});
}
return (<>{creditNotes.results.map(record => <...>}</>)
}
And this has been running fine, but recently I need to wrap around NormalList with ListPage component:
export default function ListPage(props) {
const customerId = props.match.params.customer_id;
return (<div>...<div><NormalList/></div></div>)
}
Then all the sudden I am getting this error Rendered more hooks than during the previous render.
It seems to me that setCreditNotes(data) inside getCreditNoteList is causing the error, but I don't know why.
So there are a couple of things you need to fix. First of all you should remove your function call from the useState function, you should only perform your side effects inside a useEffect hook see React Docs.
The next thing is whenever you decide to use the dependency array to your useEffect hook you should include all the dependencies of useEffect i.e all the props, state including functions inside your function component that you used inside your useEffect hook. So the Rule of Thumb is Never Lie About Your Dependencies! otherwise you will shoot yourself in the foot.
So the easiest option is to move your getCreditNoteList function inside your useEffect hook and add all the dependencies of your useEffect hook to the dependency array.
export default function NormalList({ customerId }) {
const pageSize = 20;
const [page, setPage] = useState(1)
const [searchString, setSearchString] = useState(null);
const [creditNotes, setCreditNotes] = useState({});
const [ordering, setOrdering] = useState(null);
useEffect(() => {
function getCreditNoteList() {
API.fetchCreditNoteList({
account_id: customerId,
page,
page_size: pageSize,
searchString,
ordering
}).then(data => {
setCreditNotes(data);
});
}
getCreditNoteList(),
// add ALL! dependencies
}, [page, searchString, ordering, pageSize, customerId]))
return (
<> </>
)
}
Second Option
If you want to use the getCreditNoteList function elsewhere and want to keep it outside your useEffect hook you can do that by wrapping your getCreditNoteList logic inside the useCallback hook as shown below and add the function to your dependency array inside your useEffect hook for reasons i mentioned earlier.
export default function NormalList({ customerId }) {
const pageSize = 20;
const [page, setPage] = useState(1)
const [searchString, setSearchString] = useState(null);
const [creditNotes, setCreditNotes] = useState({});
const [ordering, setOrdering] = useState(null);
const getCreditNoteList = useCallback(() => {
API.fetchCreditNoteList({
account_id: customerId,
page,
page_size: pageSize,
searchString,
ordering
}).then(data => {
setCreditNotes(data);
});
// the function only changes when any of these dependencies change
},[page, searchString, ordering, pageSize, customerId])
useEffect(() => {
getCreditNoteList(),
},[getCreditNoteList])
return (
<> </>
)
}
OK My problem was that I had 2 issues with importing the elements, first being pycharm cleverly automatically imported them wrong, and secondly being I didn't import one of the component at all.
I wish the error message could be a bit more specific than "Rendered more hooks than during the previous render".
Also I tested I didn't need to move function body getCreditNoteList inside useEffect.
Thanks to #chitova263 for spending the time to help out.

Categories

Resources