Linter or User error: Setting state from useEffect causing loop - javascript

Currently my component looks like:
const { listOfStuff = [{name:"john"},{name:"smith"}] } = props
const [peopleNames, setPeopleNames] = useState([])
useEffect(() => {
listOfStuff.forEach(userName => {
setPeopleNames(peopleNames.concat([userName.name]))
})
},[listOfStuff, peopleNames])
As you can probably see this results in an infinite loop, since peopleNames is being updated. Since its included in the dependancies array.
Now, I could remove it from the dependencies array. But my linter would yell at me.
And previous experience has told me to trust my linter over my own judgement.
I feel like im missing something fundamental here.
Ideally, I would like the peopleNames state to look something like
['john','smith']

setPeopleNames(current => current.concat([userName.name]))
Then you can remove peopleNames from the dependencies array.
The second array item returned from useState ie setState can take a function as it’s argument where the parameter of the function is the current value of the state. So you can make changes based on the current state value without referencing the first argument of useState.
Further homework:
Your useEffect code can be simplified to:
useEffect(() =>
setPeopleNames(current =>
current.concat(listOfStuff.map(stuff => stuff.name)
),
[listOfStuff]);

Related

Rewrite useEffect hook from componentDidUpdate lifecycle method

Suppose we have an input for buyer id and we want to fetch the buyer details each time the buyerId is changed.
The following code looks like this for class components
componentDidUpdate(prevProps,prevState) {
if (this.state.buyerId !== prevState.buyerId) {
this.props.fetchBuyerData(this.state.buyerId); // some random API to search for details of buyer
}
}
But if we want to use useEffect hook inside a functional component how would we control it. How can we compare the previous props with the new props.
If I write it as per my understanding it will be somewhat like this.
useEffect(() => {
props.fetchBuyerData(state.buyerId);
}, [state.buyerId]);
But then react's hooks linter suggests that I need to include props into the dependency array as well and if I include props in the dependency array, useEffect will be called as soon as props changes which is incorrect as per the requirement.
Can someone help me understand why props is required in dependency array if its only purpose is to make an API call.
Also is there any way by which I can control the previous state or props to do a deep comparison or maybe just control the function execution inside useEffect.
Deconstruct props either in the function declaration or inside the component. When fetchBuyerData is used inside the useEffect hook, then only it needs to be listed as a dependency instead of all of props:
// deconstruct in declaration
function MyComponent({ fetchBuyerData }) {
useEffect(() => {
// caveat: something is wrong with the use of `this.state` here
fetchBuyerData(this.state.buyerId);
}, [fetchBuyerData, state.buyerId]);
}
// deconstruct inside component
function MyComponent(props) {
const { fetchBuyerData } = props;
useEffect(() => {
// caveat: something is wrong with the use of `this.state` here
fetchBuyerData(this.state.buyerId);
}, [fetchBuyerData, state.buyerId]);
}
I'd assume you're rewriting your class component info functional one. Then you'd be better off including your fetch request right where you set new state.bayerId (I assume it's not an external prop). Something like:
const [buyerId, setBuyerId] = React.useState('')
const handleOnChange = (ev) => {
const { value } = ev.target
if (value !== buyerId) {
props.fetchBuyerData(value)
setBuyerId(value)
}
...
return (
<input onChange={handleOnChange} value={buyerId} />
...
The code snippet is somewhat suboptimal. For production I'd assume wrap change handler into useCallback for it to not be recreated on each render.

React.js Passing setState as prop causes a warning about using props for dependencies

I am passing state as a variable down to a component via props like to...
const [someState, setSomeState] = useState([])
PlaceDataInIndexDB({ setSomeState: setSomeState,
user: props.user })
And in the PlaceDataInIndexDB.js I have a useEffect which eventually sets the state using
useEffect(() => {
props.setSomeState([array])
}), [props.user]
The issue is that I get a warning saying I need to use props in my dependency array, however, I do not want the useEffect to run every time props change because I have a lot of other props in there. What can I do for this? This is an extremely simplified version of my code. i can post exact code but it is a lot more to look through.
And here is the warning...
React Hook useEffect has a missing dependency: 'props'. Either include
it or remove the dependency array. However, 'props' will change when
any prop changes, so the preferred fix is to destructure the 'props' object outside of the useEffect call and refer to those specific props
inside useEffect react-hooks/exhaustive-deps
It's telling you what the issue is. Generally, anything referenced inside of the useEffect function needs to also exist in the dependency array, so React knows to run it again when those things change. If the thing you're using is a property of some other object (like props), it's best to pull the values out of there prior to referencing them.
const PlaceDataInIndexDB = (props) => {
const { setSomeState, user } = props
useEffect(() => {
setSomeState([array])
}), [setSomeState, array] // <-- your example doesn't show where `array` comes from, but needed here as well
// ...
}
You can, in fact, destructure the props inline so that you never even have a reference to the entire props object:
const PlaceDataInIndexDB = ({ setSomeState, user }) => {
Note that setSomeState must also be in the dependency array. If -- and only if -- the useState is in the same component, the linter is smart enough to know that it never changes and lets you leave it out. However, if it's passed in as a prop from somewhere else, there is no way for it to know.
The linting rules for hooks are very good. By and large, unless you really know what you're doing, you can blindly follow the suggestions it makes. Eslint actually added an entire "suggestions" feature for this specific rule, which e.g. vscode will change for you if you put your cursor on the error and press ctrl+.

React setState() not setting state on certain items

I'm new to React and have some pretty simple code that is acting strange (I think).
The main app fetches a list of blog posts from a server, then passes them through props to a child component which spits the list out. By default, I'm trying to make the posts only show a preview like a title, and each post will have a state attached to it so I can keep track of which ones are fully shown or previewed.
I have the states set up like this:
const [posts, setPosts] = useState([])
const [postFullView, setPostFullView] = useState([])
The list initially is rendered as an empty list so nothing gets returned. When the data fetch finishes, it re-renders with all the posts.
I use useEffect for this in the child component:
useEffect(() => {
console.log('render') //Just to verify this got called
setPosts(props.posts) //Logs empty array 3 lines down,
//setPosts([4,5,6]) //Works fine, gets logged as [4,5,6]
console.log(props.posts) //Logs an array of 32 objects - so props is clearly not empty
console.log(posts) //Logs empty array as if setPosts did nothing, but logs [4,5,6] if I comment out setPosts(props.post) and use setPosts([4,5,6])
setPostFullView(posts.map(post => {return {id: post.id, view: false}}))
console.log(postFullView) //Will be empty since posts is empty
}, [props])
Hopefully, I explained clearly what I'm confused about - I can setState using a hard-coded array, but passing in props.posts does not do anything, even though it has content.
There is nothing wrong about your code, and the reason console.log(posts) spits empty array, it because setPosts(props.posts) is async call and not executed immediately, but tells react it should render again with new value for state.
Sometimes, like in your hardcoded array case, the code will work "fine", but it not guaranteed, for sure in production when code executed faster
yea its nothing wrong because the setState api is async did not show the changes in log but code is work properly and also if you need to check your state you can use React developer Tools extension for browser too see the state status
https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en
Actually, your first issue is about understanding the state changing and the re-rendering on ReactJS. why you do not use the first initial state in the first render just like below:
const YourComponent = ({ posts: initialPosts }) => {
const [posts, setPosts] = useState(initialPosts);
Also, there is no need to have the first line you can use it exactly on the second line initializing, like this:
const YourComponent = ({ posts: initialPosts }) => {
const [postFullView, setPostFullView] = useState(
({ id }) => ({ id, view: false }) // es6 arrow function with using restructuring assignment and returning the object
);
After all, when you are using a useEffect or other hooks APIs, please add the specific internal state or prop name to dependencies, no put all the props in the array of dependencies, it caused bad costs and make your project slow to run.

Why is my state not updating with fetched data in time for the useEffect to update my DOM with the new state?

I am new to using hooks in React. I am trying to fetch data when the component first mounts by utilizing useEffect() with a second parameter of an empty array. I am then trying to set my state with the new data. This seems like a very straightforward use case, but I must be doing something wrong because the DOM is not updating with the new state.
const [tableData, setTableData] = useState([]);
useEffect(() => {
const setTableDataToState = () => {
fetchTableData()
.then(collection => {
console.log('collection', collection) //this logs the data correctly
setTableData(collection);
})
.catch(err => console.error(err));
};
setTableDataToState();
}, []);
When I put a long enough timeout around the setTableData() call (5ms didn't work, 5s did), the accurate tableData will display as expected, which made me think it may be an issue with my fetch function returning before the collection is actually ready. But the console.log() before setTableData() is outputting the correct information-- and I'm not sure how it could do this if the data wasn't available by that point in the code.
I'm hoping this is something very simple I'm missing. Any ideas?
The second argument passed to useEffect can be used to skip an effect.
Documentation: https://reactjs.org/docs/hooks-effect.html
They go on to explain in their example that they are using count as the second argument:
"If the count is 5, and then our component re-renders with count still
equal to 5, React will compare [5] from the previous render and [5]
from the next render. Because all items in the array are the same (5
=== 5), React would skip the effect. That’s our optimization."
So you would like it to re-render but only to the point that the data changes and then skip the re-render.

Jest / React - How to test the results of useState state update in a function

I have a simple cart function that, when a user clicks to increase or decrease the quantity of an item in a shopping cart, calls a useState function to update the cart quantity in state.
const [cart, setCart] = useState([]);
const onUpdateItemQuantity = (cartItem, quantityChange) => {
const newCart = [...cart];
const shouldRemoveFromCart = quantityChange === -1 && cartItem.count === 1;
...
if (shouldRemoveFromCart) {
newCart.splice(cartIndex, 1);
} else {
...
}
setCart(newCart); //the useState function is called
}
so in jest, I have a function that tests when a user sets a cart item's quantity to zero, but it does not yet remove the item from the cart, I'm assuming because it has not yet received the results of setCart(newCart):
test('on decrement item from 1 to 0, remove from cart', () => {
const [cartItemToDecrement] = result.current.cartItems;
const productToDecrement = result.current.products.find(
p => p.id === cartItemToDecrement.id
);
act(() => {
result.current.decrementItem(cartItemToDecrement);
});
act(() => {
result.current.decrementItem(cartItemToDecrement);
});
...
expect(result.current.cartItems).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: cartItemToDecrement.id,
count: cartItemToDecrement.count - 2,
inventory: productToDecrement.inventory + 2
})
])
);
});
});
This test passes, because the cart now contains an item whose quantity has dropped to zero. But really it shouldn't, because from the splice operation in onUpdateItemQuantity our cartItems array should now not include the object at all. How do I verify in my jest test that this removal is happening (the react code works properly).
I do not really understand the relation between the test and your onUpdateItemQuantity very much because the context you provided is not sufficient to me.
But from your question, there are 2 clues which may help to save your time.
You may know setCart from useState is not synchronous, so that if you try to access cart from useState at the same frame, it shouldn't reflect the change even though you ensure setCart called. useEffect is a good solution. You can find the doc of useEffect [https://reactjs.org/docs/hooks-effect.html][1], any change should be reflected in useEffect, perhaps you can put your test there.
Add an extra variable myCart to store cart and a function setMyCart. Instead of calling setCart in your code, call setMyCart. setMyCart is like,
function setMyCart(newCart)
{
myCart = newCart;
setCart(myCart); // this is for triggering React re-render
}
then use myCart which can reflect the change immediately for testing.
The only purpose of the additional code in the 2nd point, is when we still rely on the re-render mechanism of React, we use our own Model myCart for our particular logic rather than cart state from React which is not only for View but also used for our logic on an inappropriate occasion.
Testing for state changes is a challenge, because it doesn't produce any specific output (except whatever impacts your render).
The key is to narrow the testing scope to your responsibilities, versus testing React. React is responsible for making sure that useState updates the state properly. Your responsibility is to make sure you are using the data properly and sending the correct data back. So instead of testing useState, test how your component responds to the state it is given, and make sure you are setting the correct state.
The answer to to mock useState. Give it an implementation that returns the initial value passed to it, with a mock function that lets you read what data is being saved.
More detailed answer here:
https://dev.to/theactualgivens/testing-react-hook-state-changes-2oga
HTH!
If you need to test every state update using useState(), you can do so by implementing a useEffect function that listens specifically for that state change, in your case, cart:
useEffect(() => {
test(...)
), [cart]);
And so you can do the test inside the useEffect() function.
That will ensure that you have the correct value of cart and it will be called every time cart is updated.
It's totally unclear how onUpdateItemQuantity is called. And even what it does exactly. Never the less, you claim that toEqual(...[{...,count:cartItemToDecrement.count - 2,...}]...) passes. That probably means that onUpdateItemQuantity is called inside of decrementItem, inside of onUpdateItemQuantity there is something like newCart[cartIndex].count--, and if toEqual() passes, that means that setState() successfully updates the state, and after act() you have the actualized state. And if test's name is correct and initial value of cartItemToDecrement.count === 1 that should mean that cartItemToDecrement.count - 2 === -1 and you never go inside of if (shouldRemoveFromCart). So maybe the issue is not with the test, but with the code.

Categories