I am reading this code snipper https://reactjs.org/docs/hooks-faq.html#how-can-i-do-data-fetching-with-hooks linked from react documentation page https://reactjs.org/docs/hooks-faq.html#how-can-i-do-data-fetching-with-hooks
I don't understand this piece of code with the returned function that modifies the ignore variable
useEffect(() => {
let ignore = false;
async function fetchData() {
const result = await axios('https://hn.algolia.com/api/v1/search?query=' + query);
if (!ignore) setData(result.data);
}
fetchData();
return () => { ignore = true; }
}, [query]);
once the function that is passed to useEffect is called a second time, doesn't ignore reset back to false leaving the ignore = true completely useless. The code above does not compute for me. Thanks for explaining.
The ignored variable is in function scope, current effect's ignored will be a different instance with the one in the next effect.
If an effect returns a cleanup function, React invokes it when the component unmounts or before the effect gets re-run due to a dependency change (if query were to change in this case).
The axios fetch call is asynchronous, so it's possible that the component will unmount or a dependency (e.g. query) will have changed before the data request completes.
If that happens, the returned function will have set ignore to true and the fetchData function won't try to call setData on the unmounted component. (Attempting to update state on an unmounted component causes an error.)
From the docs:
When exactly does React clean up an effect? React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.
Due to the way javascript closures work, the cleanup function can access the ignore variable in the enclosing scope.
When the effect runs again, a new ignore variable gets declared with a new cleanup function that references the new variable.
Your understanding of the functional call and variable assignment is correct, but you forgot the power of the async call :)
useEffect is called whenever it is needed (e.g. componentDidMount, componentDidUpdate by query keyword), so we roughly have
ignore = false
function fetchData is now defined
function fetchData is called
keep the reset of useEffect in your mind for a sec.
let's say something happens which causes a re-render e.g. prop-change (but other than query) or even call that setData with newly fetched data.
your useEffect will affect nothing, cause query shallow-comparison can not find a change with the previous one. So the purpose of the ignore is not involved in this case, cause it is not going to change at all.
Also if the query change in any way the function execution would roughly (ignoring event loop, macro task everything for the moment) be something like:
// ignore is false and for some reason query changes...
(() => { ignore = true })() // ignore = true
// continue running useEffect again.
// lets think nothing else will happend in gap
ignore = false // ignore = false
The only possible scenario I can think about is when your component is going to be unmounted when your useEffect fallback is being called. So here is when that async keyword wants its dignity back ๐๏ธ.
Remember: The async function is not return anything by your command.
Imagine the data arrive when your component is fully removed from the DOM, here is the role which that ignore=true plays in this useEffect.
Preventing setData on unmounted component
Related
When the state called loading is set to true and a re-render of the component is caused, where does the function continue?
Does it run console.log(1) again, just that it's that fast that I can't track it? Or does it contiune with console.log(2)?
Or is the re-render happening after console.log(2) while the asynchronous operation is running?
The same question appears regarding the moment loading is set to false again.
Will it re-render after console.log(3) and console.log(4) have run or between those as the order in the code would suppose?
If there was no async operation running between the state changes, would the Spinner even appear at some point?, or would the entire function run before it re-renders and therby the state true not even trigger a re-render, because the state changes get "bundled" which would mean that it would only re-render when the state is set to false again?
Sorry for the complicated formulation but I don't know any other way to express myself.
I struggle with the idea that a re-render means that the entire functional component is re-run, because a function call could thereby be interrupted. I just don't undestand how and where it conitunes and if that is consistent, because it should be IMO.
ยดยดยด
export default function IndexComponent() {
const [loading, setLoading] = useState(false)
const doSomething = async () => {
console.log(1)
setLoading(true) //re-renders and Spinner appears
console.log(2)
await new Promise(resolve => setTimeout(resolve, 5000));
console.log(3)
setLoading(false)//re-renders and Spinner disappears
console.log(4)
}
return (
<div>
<button onClick={doSomething}>Login</button>
{loading && <div>Spinner</div>}
</div>
)
}
ยดยดยด
You may be over-thinking this. There's no magic happening in React which in any way changes how JavaScript works. When a function is invoked, it executes. Nothing in React interrupts that function in any way. And a re-render does indeed re-invoke the component function.
So in this case, every time the button is clicked, doSomething is invoked and executes all of the logic therein.
What may be confusing you regarding re-renders in this specific case is the fact that doSomething is asynchronous and is not awaited. So after this executes:
setLoading(true)
A state update has been queued. It won't be processed until the thread is free to process asynchronous operations. That can happen here:
await new Promise(resolve => setTimeout(resolve, 5000));
It depends on whether the function itself is being awaited too. And in the code shown it isn't. So this operation puts another asynchronous operation on the queue, and frees up control to start processing the asynchronous operations already in place. Which, in this case, is a state update and a re-render.
Then when the above Promise resolves, the remaining operations in the function are processed. Which also includes a state update, which triggers a re-render.
Alternatively, suppose you didn't have anything asynchronous happening. Suppose you just did this:
console.log(1)
setLoading(true)
console.log(2)
setLoading(false)
console.log(3)
In this case only one re-render is (potentially) triggered. Multiple state updates are made, but states updates are asynchronous and batched. So they're all waiting until the thread is available, and then the batch of updates are processed. In this case the net result of those updates is no change, so there may be no re-render.
I hope i understood your questions correctly.
A call to setState is also asynchon and the changes are getting batched for performance gains. So your doSomething function probably runs to the end before the state Changes take effect.
Here is the respective Article in the React Docs:
https://reactjs.org/docs/faq-state.html#what-does-setstate-do
My functional component is structured as follows:
// Initialization
useEffect(() => {
fetchDetailedUserInfo();
}, []);
// Update on initialization of detailedUserInfo
useEffect(() => {
if (detailedUserInfo) {
//...
setInitialFormValues(..);
}
}, [detailedUserInfo]);
// Update on initialization of initialFormValues
useEffect(() => {
//...
}, [initialFormValues]);
// Return JSX
return ( .. );
What determines when the render (return) runs in between all these useEffects, and how are the useEffects ordered among themselves? What is the exact flow?
According to docs:
By default, effects run after every completed render, but you can choose to fire them only when certain values have changed.
The clean-up function runs before the component is removed from the UI to prevent memory leaks. Additionally, if a component renders multiple times (as they typically do), the previous effect is cleaned up before executing the next effect. In our example, this means a new subscription is created on every update. To avoid firing an effect on every update, refer to the next section.
A lot of people tend to miss the above statement on the documentation.
There are two behaviours to be noticed.
When there is an empty dependency array []
In this case the cleanup effect will only get called when the component is going to unmount
When there is some dependency in the dep array
In this case, the cleanup effect is called everytime the useEffect is triggered due to the update phase, it will be called first, then the callback function will be called, so as to ensure that any cleanup is done before the next callback is called.
The main reason for this confusion is that when someone comes from class components this is not the case, and they tend to follow the lifecycle diagram provided.
The hooks LC diagram is not really official, and has a small flaw
https://github.com/Wavez/react-hooks-lifecycle/issues/5
I had raised an issue on the github repo as well with the correction.
Its still open though.
Do checkout the docs here
https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect
why dont you do something like this instead?
useEffect(() => {
// you can return the promise inside the fetchDetailedUserInfo function, and ensure that you return the reponse. make sure to add a catch block or handle that in the response.
fetchDetailedUserInfo()
.then(res=>{
setInitialFormValues(...)
})
}, []);
The first useEffect runs when the component is first rendered, so will fire first. The second useEffect will fire whenever detailedUserInfo has changed its value (assuming its not null/false, etc.). The third useEffect will fire whenever initialFormValues has changed its value.
In the following code from reactjs.org:
useEffect(() => {
function tick() {
// Read latest props at any time
console.log(latestProps.current);
}
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []); // This effect never re-runs
as my brain compiles it, the effect created is a "wait one second then do something", but the setInterval being async itself, it returns immediately then useEffect return its closure callback. React being aware of states changes only and not of actions (it doesn't know what was launched in the useEffect, isn't it? How could he know!), I suppose that it'd fire the closure callback directly on return and then prevent the tick() function to be fired even ounce... but it's not the case. How comes ? How React knows what to wait before firing the closure callback returned by useEffect?
While I don't know exactly what happens in the background, for implementation sake you need only to know that the return callback of a useEffect is only called when the effect is re-ran, or more specifically, after "closing" the previous effect-run and before the new effect running. Depending on the effect's dependencies, it can be on every render, or (as in the example you posted) only when the component is unmounted.
It's useful to think that functional components are just functions, so unless the function (the component) is called again (a re-render or other lifecycle change), the effect is "stopped", there's no magical parallel process. I would risk saying that react checks the hooked effects on a component in pre and post render. Depending on dependencies and the effect's details, it knows whether it should call the return callback or not call the effect at all.
See this sandbox I created where I demo these two most extreme cases: effect on every render, and effect only on mount/unmount. Check the sandbox console to understand the behavior. Try to change the parent's effect dependencies to [count] and see the differences.
PS: when I started using hooks, this article helped me a lot https://overreacted.io/a-complete-guide-to-useeffect/
I'm trying to do an ajax call when a state changes, and then set another state to the result of that ajax call.
const [plc, setPlc] = useState(null);
const [tags, setTags] = useState([]);
...
useEffect(()=>{
if(plc != null) {
myAjaxPromiseFunction(plc).catch((err)=>{
console.log(err);
}).then((tags)=>{
setTags(tags);
});
}
}, [plc]);
For some reason, this results in an infinite loop. However, when I remove the setTags(tags); statement it works as expected. It appears that the setTags function is causing the effect hook to update, but tags is not a part of the dependencies of this effect hook.
FYI, the tags variable is supposed to be a list of objects.
EDIT:
I have one other useEffect in my code.
useEffect(() => { // Update which tags are logged
if(plc != null) {
anotherAjaxFunction(plc, tags).catch(err => {
toaster.show({
message: err,
intent: "danger"
});
}).then(tags => {
setTags(tags);
});
}
}, [plc, updateLoggedFlag]);
However, this useEffect is not dependant on tags, and the issue still occurs if I remove this block of code.
Side note: The updateLoggedFlag is a variable I was planning to use to force the effect to update as needed, but I haven't used it anywhere yet.
Another EDIT: Link to reproduction using code sandbox: https://codesandbox.io/s/cranky-ives-3xyrq?file=/src/App.js
Any ideas? Thanks for the help.
In short: move TagColumn outside of App, as it is here.
The problem is not connected to the effects declaration, it's due to the component declaration.
In your sandbox, the TagColumn was defined inside of App. It means that every time App was rendered (in particular, every time its state is changed), the const TagColumn was assigned a different value. React can't see that it is the same component every time - after all, it's not really the same, since the captured scope is changing.
So, App renders a new TagColumn. React sees that component is created from scratch, renders it, and, in particular, invokes its hooks, including every useEffect. Inside useEffect, you use callback to change the state of App. App rerenders, creates yet another component TagColumn, which is again rendered for the first time in its life, calling every useEffect... and the cycle continues.
In general, you shouldn't capture anything non-constant in the functional component scope. Instead:
if the value is generated by external context (App, in this case) - pass it as props;
if the value is generated by the component itself - use useState, useRef or other hooks, as necessary.
The problem I think with your code is that it does not include tags in the dependency array. Conceptually, this should work fine, as mentioned before by other developers, but React documentation states that, all the state variables and props used inside the callback, should be present inside the dependency array, otherwise it can cause bugs. Have a look at it here. Hooks FAQ
Regarding the official documentation clean-up function of react with useEffect help should be called only once and before component will be destroyed.
So regarding this information I made a component and put some code for example let it be the next one
useEffect(() => {
console.log('didmount')
return () => {
console.log('will unmount2')
}
})
I expect to see a several didmount consoles, depending on how many times I'm going to changes props of it and only one "will mount2" before I change my route, so component will be destroyed,
but actual result is next
What I've just missed ?
You didn't understand or fully read the docs.
Additionally, if a component renders multiple times (as they typically do), the previous effect is cleaned up before executing the next effect. In our example, this means a new subscription is created on every update.
Changing the props causes the component to rerender. Which in turn causes the clean-up function to be executed.
For your example this means that you should see serveral didmount and will unmount2 in the console depending on how many times the component did rerender.
hello Artem in your function you are missing the second param wich is the dependency array, without this array will cause an infinite loop ill suggest to try this
// this will run exactly when the unmount component, not when the effects finishing running
useEffect(() => {
return () => {
console.log('unmount')
}
}, [])