setState() does not stop rendering - javascript

I want to update useState array values by calling a function that maps through an array (called from the database) and the useState array will be updated for each item in the (database array) so I have tried the following approach:
const [snapshots, setSnapshots] = useState();
const [items, setItems] = useState([]);
// *** get from the database ***** //
useEffect(()=> {
db.collection("users").doc("4sfrRMB5ROMxXDvmVdwL").collection("basket")
.get()
.then((snapshot) => {
setSnapshots(snapshot.docs)
}
) ;
}, []);
// *** get from the database ***** //
// *** update items value ***** //
return <div className="cart__items__item">
{snapshots && snapshots.map((doc)=>(
setItems([...items, doc.data().id]),
console.log(items)
))
}
</div>
// *** update items value ***** //
but the following error appears:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
I have tried to console.log the result to see the check the issue and the Items array was logged in the console continuously I have tried to include the code in a useEffect but it did not work as well .

Never call a state setter at the top level of your component function. With function components, the key thing to remember is that when you change state, your function will get called again with the updated state. If your code has a state change at the top level of the function (as yours does in the question), every time the function runs, you change state, causing the function to run, causing another state change, and so on, and so on. In your code:
const initialArray = []; // *** 1
const [Items, setItems] = useState(initialArray) // *** 2
initialArray.push("pushed item")
setItems(initialArray) // *** 3
Creates a new array every time
Only uses the first one to set the initial value of Items when the component is created
Sets new array in state, causing the function to be called again
Instead, you should be setting state only in response to some change or event, such as a click handler, or some other state changing, etc.
Also note that you must not directly modify an object (including an array) that you have in state. Your code doesn't technically do that (since there's a new initialArray every time), but it looks like what you meant to do. To add to an array in state, you copy the array and add the new entry at the end.
An example of the above:
function Example() {
const [items, setItems] = useState([]);
const clickHandler = e => {
setItems([...items, e.currentTarget.value]);
};
return <div>
<div>
{items.map(item => <div key={item}>{item}</div>)}
</div>
<input type="button" value="A" onClick={clickHandler} />
<input type="button" value="B" onClick={clickHandler} />
<input type="button" value="C" onClick={clickHandler} />
</div>;
}
(Slightly odd UI just to keep the code example simple.)
Note that conventionally Items would be called items.
Re your update:
That code calls setItems at the top level of the function, so it has the problem above. Instead, you do that work in the useEffect querying the database.
There's no reason to call setItems repeatedly during the map operation.
The code should handle the component unmounting while the DB operation is outstanding
The code should actually render something in the map in the JSX
The code should handle errors (rejections)
E.g., something like this:
const [snapshots, setSnapshots] = useState();
const [items, setItems] = useState(); // *** If you're going to use `undefined`
// as the initial state of `snapshots`,
// you probably want to do the same with
// `items`
useEffect(()=> {
let cancelled = false;
db.collection("users").doc("4sfrRMB5ROMxXDvmVdwL").collection("basket")
.get()
.then((snapshot) => {
// *** Don't try to set state if we've been unmounted in the meantime
if (!cancelled) {
setSnapshots(snapshot.docs);
// *** Create `items` **once** when you get the snapshots
setItems(snapshot.docs.map(doc => doc.data().id));
}
})
// *** You need to catch and handle rejections
.catch(error => {
// ...handle/report error...
});
return () => {
// *** The component has been unmounted. If you can proactively cancel
// the outstanding DB operation here, that would be best practice.
// This sets a flag so that it definitely doesn't try to update an
// unmounted component, either because A) You can't cancel the DB
// operation, and/or B) You can, but the cancellation occurred *just*
// at the wrong time to prevent the promise fulfillment callback from
// being queued. (E.g., you need it even if you can cancel.)
cancelled = true;
};
}, []);
// *** Use `items` here
return <div className="cart__items__item">
{items && items.map(id => <div>{id}</div>)/* *** Or whatever renders ID */}
</div>;
Note that that code assumes that doc.data().id is a synchronous operation.

What you are seeing here is standard react lifecycle behavior. Your component will be mounted and then render (run alle the code inside your component). After rendering the first time it "listens" to changes to the values that you handle in your component and re-renders if any changes are detected.
Your case:
const initialArray = [];
const [Items, setItems] = useState(initialArray)
initialArray.push("pushed item")
setItems(initialArray)
I see 2 bad things here:
You modify the array which you use as the initial value of your state and keep using that same array to update your state.
You push a new item and update the state on each render when calling setItems(initialArray)
Lets focus on the second one, since thats the one that causes your issues. If you want to avoid the endless render cycle then you should move your setItems() call to a method which doesn't run on each render. Before functional components this would be done in the componentDidMount() function. In a functional component this is done using the useEffect hook:
useEffect(() => {
// Your code here
}, [])
Notice the empty array supplied to useEffect. This array lists which dependencies should cause this useEffect to run. If you leave it empty it will only run once and act like the old componentDidMount() function.
So to solve your issue of endless rendering, you need to move setItems() into the useEffect hook.

Related

React component does not re-render under Jest on state change

Component:
const MyComponent = props => {
const {price} = props;
const result1 = useResult(price);
return (
<div>...</div>
)
}
Custom Hook:
export const useResult = (price) => {
const [result, setResult] = useState([]);
useEffect(() => {
const data = [{price: price}]
setResult(data);
}, [price]);
return result;
};
Jest test:
it('should ...', async () => {
render(
<MyComponent price={300}/>)
)
await waitFor(() => {
expect(...).toBeInTheDocument();
});
});
What it does happen with the above code is that MyComponent, when running the test, renders only once instead of two (when the application runs). After the initial render where result1 is an empty array, useEffect of useResult is running and since there is a state change due to setResult(data), I should expect MyComponent to be re-rendered. However, that's not the case and result1 still equals to [] whereas it should equal to [{price:300}].
Hence, it seems custom hooks under testing behave differently than the real app. I thought it would be okay to test them indirectly through the component that calls them.
Any explanation/thoughts for the above?
UPDATE
The issue that invoked the above erroneous behaviour was state mutation!! It worked with the app but not with the test! My mistake was to attempt to use push in order to add an element to an array that was a state variable...
Well, it seems that you are asking a very specific thing about testing a custom hook. In that case, I also had some issues in the past testing custom hooks through #testing-library and a different package was created (and recently incorporated into the #testing-library) that provides the renderHook() function for testing custom hooks. I suggest you to test that.
Original Package (do not use it. Use directly the TL one)
Docs about the renderHook() call inside the TL docs
You can read more about it in this blog post from Kent C. Dodds.
I also suggest you create a "state change" to test your component and test the hook with the renderHook().
Here is a simple codesandbox with some tests for a component similar to your case.
Original Answer
Essentially, your test is not waiting for the component to perform the side effects. There are 2 ways of waiting for that:
Using waitFor()
import { waitFor, screen } from '#testing-library/react'
// ...
// add the `async` before the callback function
it('should ...', async () => {
render(<MyComponent price={300}/>);
await waitFor(() =>
expect(screen.getByText('your-text-goes-here')).toBeInTheDocument()
)
});
Using the findBy* query from RTL, that returns a Promise (read the Docs here) and is a combination from the waitFor and getBy* query (read docs here)
import { screen } from '#testing-library/react'
// ...
// add the `async` before the callback function
it('should ...', async () => {
render(<MyComponent price={300}/>);
expect(await screen.findByText('your-text-goes-here')).toBeInTheDocument();
});
Step 1: the code being tested
If, as mentioned in the comments of the question, the operation inside the effect is synchronous, then using useEffect for setting this state based on the props is undesirable in all cases. Not only for testing.
The component will render, update the DOM and immediately need to re render the following frame because it's state was updated. It causes a flash effect for the user and needlessly slows the app down.
If the operation is cheap, it's way more efficient to just execute it on every render.
If the operation can be more expensive, you can wrap it in useMemo to ensure it only happens when there's changes to the inputs.
export const useResult = (price) => {
return useMemo(
// I assume this is a stub for a expensive operation.
() => [{price: price}],
[price]
);
};
If, for some obscure reason, you do need to do this in an effect anyway (you probably don't but there's edge cases), you can use a layoutEffect instead. It will be processed synchronously and avoid the flashing frame. Still wouldn't recommend it but it's a slight improvement over a regular effect.
Step 2: Testing
If you changed the component to not use an effect, it should now be correct from the first render, and you don't have the problem anymore. Avoiding having a problem in the first place is also a valid solution :D
If you do find the need to flush something synchronously in a test, there's now the flushSync function which does just that.
Perhaps it would also flush the state update in the effect, causing your test to work with no other changes. I guess it should, as new updates triggered by effects while flushing should continue to be processed before returning.
flushSync(() => {
render(
<MyComponent price={300}/>)
)
})
In any case there's no point doing this if you can instead improve the component to fix the additional render introduced by setting state in an effect.
you can do:
The test will have to be async: it('should ...', async() => { ....
await screen.findByText('whatever');
This is async so it will wait to find whatever and fail if it can't find it
or you can do
await waitFor (() => {
const whatever = screen.getByText('whatever');
expect(whatever).toBeInTheDocument();
})
You are not waiting for the component to be rerendered
import { waitFor, screen } from 'testing-library/react'
it('should ...', async () => {
render(
<MyComponent price={300}/>)
)
await waitFor (() => {
// check that props.price is shown
screen.debug() // check what's renderered
expect(screen.getByText(300)).toBeInTheDocument();
});
});

Why does this React's useEffect() fires twice after loaded?

I have this very simple component called Editor:
export default function Editor() {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [currentData, setCurrentData] = useState({});
useEffect(() => {
fetch("http://localhost:4000/pages")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setCurrentData(result);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}, [])
if (error) {
return (
<LoadingError value={error.message} />
);
} else if (!isLoaded) {
return (
<Loading />
)
} else {
console.log(currentData);
}
return ( <div></div> );
}
Notice the last else condition, where my currentData is logged into the console.
What happens when this component is rendered is that my console.log() fires twice! The first time my currentData object is empty. The second time, the object properly contains all the data from my API.
However, as my console.log() should fire only when there is no error (the first if condition), and only when the data is loaded (the second else if condition), how is it possible that I end up with empty object in the console the first time? What am I doing wrong?
Your useEffect only fires once. You're seeing the console.log output twice because you're doing updateState twice. Change the order to see the expected behaviour you're looking for
This is what's happening right now:
setIsLoaded(true); // Causes 1st rerender and so console.log executes with []
setCurrentData(result); // Causes second rerender and console.log has data.
What you want:
setCurrentData(result); // Causes 1st rerender but does not reach the else statement
setIsLoaded(true); // Reaches the else statement and currentData is already set
This will make sure your currentData is set before isLoaded is set to true
As of now, React does not batch update state calls in the above scenario, but this will change once React v18 is released.
Why does this React's useEffect() fires twice after loaded?
It doesn't.
The function Editor runs again because you call setIsLoaded and setCurrentData once the promise in the effect (which runs only once) resolves.
I have a feeling that React only batches state updates for certain handlers, like onClick and others, but I have feeling that when state updates occur in the callback of a promise, they dont get batched, so you will have two renders happening in your case.
edit - which is probably what #Harkunwar's answer is alluding to, in that the order in which you update your state matters

Execute useEffect() only on specific setState()

I am using a hook component and several state variables. I have read about using useEffect() with params to get a kind of callback after updating a state. Example:
export const hookComponent = () => {
const [var, setVar] = useState(null);
useEffect(() => {
//do things
}, [var])
}
In this example, useEffect() would be executed on every setVar() call. In my case, I do not want to execute useEffect() everytime, but only on specific occasions.
I would like to give the setVar() some kind of information which I can use in useEffect() like setVar(newValue, true).
Note: I do not want to store this information in var.
Is there a way to do this?
Like Nizar said, simple conditional check on 'var' in useEffect
If expensive calc you can
const expensiveValue = useMemo(() => {
// other logic here if needed
// could even be simple return var=='x'?true:false, although this would be easier to do in the useEffect hook?
return computeExpensiveValue(var);
},[var]);
useEffect(() => {
//do things
//expensiveValue only changes when you want it to from the memo
}, [expensiveValue])
Thank you sambomartin and Nizar for your input.
For everyone looking for an answer:
After some further research I found 3 possible solutions:
Use a class component. If you really are dependent on that state update to be completed switch to a class component, which allows you to give the setState() a callback as a second param.
Use the useRef hook to determine where your state update is comming from. You can use this information in the useEffect() method.
Get independent from the state. I used this solution and externalized my callback function with the drawback of giving it every parameter on every call, although they are present in the component the states are saved.
As far as I know, the useEffect only triggers if the dependency value changes, not simply by executing setValue.
I offer you three solutions, the first one, close to what you want but without using useEffect hook, the second one is an extension of the first one, that may be required if you need control over the previous state, and the third, more general, like comments say, though it won't be triggered if the state is the same, even if you execute setValue.
First solution: Wrap your set value with another function that definitely controls what may happen after or before the new state:
export default function MyComponent() {
const [state, setState] = useState(null);
const handleChangeSetState = (nextState, flag) => {
if (flag) {
specialUseCaseCb();
}
setState(nextState);
};
return <div>{/* ... */}</div>;
}
Second solution: Wrap your set value with another function, like in the solution 1, and ask for the previous or next state within setState inner callback:
export default function MyComponent2() {
const [state, setState] = useState(0);
const handleChangeSetState = (increment, flag) =>
setState((prevState) => {
const nextState = prevState + increment;
// you may need prevState or nextState for checking your use case
if (flag) {
specialUseCaseCb();
}
return nextState;
});
return <div>{/* ... */}</div>;
}
Third solution: use useEffect hook to follow changes, remember though that setState won't re-trigger useEffect hook if the state is the same:
export default function MyComponent3() {
const [state, setState] = useState("");
// notice that this will only be triggered if state changes
useEffect(() => {
if (state !== "my-special-use-case") return;
specialUseCaseCb();
}, [state]);
return <div>{/* ... */}</div>;
}

React - Updating data in dispatch from useEffect

I am trying updating data in dispatch in useEffect but showing warning in console
React Hook useEffect has missing dependencies: 'dispatch', 'id', and 'state.selectedHotel'. Either include them or remove the dependency array react-hooks/exhaustive-deps
code
import { GlobalContext } from "../../../context/globalContext";
const HotelDetail = () => {
const [state, dispatch] = useContext(GlobalContext);
const { id } = useParams();
useEffect(() => {
const hotelData = async () => {
try {
let response = await ServiceGetAllHotels();
let hotel = response.hotels.filter(hotel => {
return hotel.hotelUserName === id;
});
dispatch({
type: "UPDATE",
payload: { selectedHotel: hotel[0] }
});
}catch(){}
};
}, [])
};
But warning message disappear when I add this (below code)
useEffect(() => {
.....
}, [dispatch, state.selectedHotel, id])
I dont understand why this error/warning , why error disappear when I add this ? Please help Can I go with this code?
Its not an error but a warning that can save you from bugs because of useEffect hook not running when it was supposed to.
useEffect hook, by default, executes after:
the initial render
each time a component is re-rendered
Sometimes we don't want this default behavior; passing a second optional argument to useEffect hook changes the default execution behavior of useEffect hook. Second argument to useEffect hook is known as its dependency array that tells React when to execute the useEffect hook.
Run "useEffect" once, after the initial render
We can achieve this by passing an empty array as the second argument to the useEffect hook:
useEffect(() => {
// code
}, []);
This effect will only execute once, similar to componentDidMount in class components.
Run "useEffect" everytime any of its dependency changes
When the code inside the useEffect depends on the state or a prop, you sometimes want useEffect to execute every time that piece of state or prop changes.
How can we tell React to run the effect every time a particular state or prop changes? By adding that state or prop in the dependency array of the useEffect hook.
Example:
Imagine a Post component that receives post id as a prop and it fetches the comments related to that post.
You might write the following code to fetch the comments:
useEffect(() => {
fetch(`/${props.postId}`)
.then(res => res.json())
.then(comments => setComments(comments))
.catch(...)
}, []);
Problem with the above code:
When the Post component is rendered for the first time, useEffect hook will execute, fetching the comments using the id of the post passed in as the argument.
But what if the post id changes or the post id is not available during the first render of the Post component?
If post id prop changes, Post component will re-render BUT the post comments will not be fetched because useEffect hook will only execute once, after the initial render.
How can you solve this problem?
By adding post id prop in the dependency array of the useEffect hook.
useEffect(() => {
fetch(`/${props.postId}`)
.then(res => res.json())
.then(comments => setComments(comments))
.catch(...)
}, [props.postId]);
Now every time post id changes, useEffect will be executed, fetching the comments related to the post.
This is the kind of problem you can run into by missing the dependencies of the useEffect hook and React is warning you about it.
You should not omit any dependencies of the useEffect hook or other hooks like: useMemo or useCallback. Not omitting them will save you from such warnings from React but more importantly, it will save you from bugs.
Infinite loop of state update and re-render
One thing to keep in mind when adding dependencies in the dependency array of the useEffect is that if your are not careful, your code can get stuck in an infinite cycle of:
useEffect --> state update ---> re-render --> useEffect ....
Consider the following example:
useEffect(() => {
const newState = state.map(...);
setState(data);
}, [state, setState]);
In the above example, if we remove the state from the dependency array, we will get a warning about missing dependencies and if we add state in the array, we will get an infinite cycle of state update and re-render.
What can we do?
One way is to skip the state as a dependency of the useState hook and disable the warning using the following:
// eslint-disable-next-line react-hooks/exhaustive-deps
Above solution will work but it's not ideal.
Ideal solution is to change your code in such a way that allows you to remove the dependency that is causing the problem. In this case, we can simply use the functional form of the setState which takes a callback function as shown below:
useEffect(() => {
setState(currState => currState.map(...));
}, [setState]);
Now we don't need to add state in the dependency array - problem solved!
Summary
Don't omit the dependencies of the useEffect hook
Be mindful of the infinite cycle of state update and re-render. If you face this problem, try to change your code in such a way that you can safely remove the dependency that is causing the infinite cycle
The useEffect hook accepts two arguments. The first one is a classic callback and the second one is an array of so called "dependencies".
The hook is designed to execute the callback immediately after component has been mount (after elements have been successfully added to the real DOM and references are available) and then on every render if at least one of the values in the dependencies array has changed.
So, if you pass an empty array, your callback will be executed only once during the full lifecycle of your component.
It makes sense if you think about it from a memory point of view. Each time that the component function is executed, a new callback is created storing references to the current execution context variables. If those variables change and a new callback is not created, then the old callback would still use the old values.
This is why "missing dependencies" is marked as a warning (not an error), code could perfectly work with missing dependencies, sometimes it could be also intentional. Even if you can always add all dependencies and then perform internal checks. It is a good practice to pass all your dependencies so your callback is always up to date.

How to get updated state in useEffect when using update loop

So as you probably know, in normal mode, we use update dependencies to get notice when the state updated, like this:
const [val, setVal] = useState();
useEffect(() => {}, [val]]);
But in my case, I have an array in my state and I'm trying to update it in a loop in my useEffect like this:
const [val, setVal ] = useState([...]);
useEffect(() => {
anotherArr.forEach(i => {
// get val and modify some indexes
setVal(modifiedValuesArray);
}
}, []);
In this case, every time forEach loop runs, I'm getting the initial val (I know because val is not a
dependency of useEffect) but if I put it as a dependency, it will update twice. what is the solution for this?
EDIT: Basically, I mean when I update state in a round of loop in useEffect, on the next round, I'm not getting the updated state but the initial state before entering the loop. And I know, that is because of the nature of useEffect which gives us a memorized value of state (since we didn't pass it as a dependency to avoid the additional execution), but what is the solution in these types of scenarios.
I came across this answer:(https://stackoverflow.com/a/59422750/2728431) and it solved my problem.
for getting updated state, (as #usafder said in a comment), we need to pass state as a value in an arrow function just like this:
const [val, setVal ] = useState([...]);
useEffect(() => {
anotherArr.forEach(i => {
setVal(val => {
// modify based on provided val on arrow function
return modifiedValuesArray
});
}
}, []);
Whenever you need to update the state using its current value, you need to send in a function instead to the state setter which would give you the updated current value of the state as a param. So in your case it would be something like below:
const [val, setVal ] = useState([...]);
useEffect(() => {
anotherArr.forEach(i => {
setVal((currVal) => {
let modifiedValuesArray = [];
// your update logic here (use currVal instead of val)
return modifiedValuesArray;
});
});
}, []);
Use setVal only once, not during an forEach loop. setState is async so you can't depend on it like its synchronous. In your example setVal will actually be executed some time in the future.. Do you maybe have a codesandbox example?
EDIT: You don't get updated state on "next round". setState will be executed N times, and will put it in an update queue, and React will probably only update the last setState value for optimisation. Also, your example useEffect will run only once..

Categories