Why don't React component functions break/terminate when state is changed? - javascript

function App() {
const [buttonClick, setButtonClick] = useState('No')
async function handleClick(){
setButtonClick('Yes')
const sleep = (ms) ={return new Promise(resolve => setTimeout(resolve, ms))}
await sleep(2000)
console.log('this ran after state was changed')
}
return(
<>
<button onclick={handleClick}>Click me</>
{buttonClick}
</>
)
}
How come if I click the button, which sets the state of buttonClick, does it not break out of the function? It still sleeps for 2 seconds then logs my message in the console.
My thinking is that since it causes a component re-render, it would lead me to believe that the function App is returning and would break out of the handleClick function.
My thoughts on why this might be is that it might be when React is compiled it is just storing the value of the return somewhere else, and not actually returning the broader function App().

This has more to do with how Javascript works rather than React, I'll do my best to explain:
When you click the button your handleClick function is invoked.
handleClick is now on the call stack and will not terminate until either a value is returned or an error is thrown.
The first instruction in handleClick is to update the state, however, updating the state will not terminate the invokation of handleClick and even if the component were to unmount before it had a chance to finish, any call to setButtonClick on an unmounted component would result in what is known as a memory leak.
Furthermore the setTimeout and async nature of this function will end up in various parts of the event loop, but that has little to do with the original question.
You can learn more about the nature of Javascript functions here.

Related

When is useEffect cleanup called?

I'm trying to learn about react's useEffect cleanup.
I came across this example on how to use cleanup.
(reference: https://blog.logrocket.com/understanding-react-useeffect-cleanup-function/).
const [data, setData] = useState({})
useEffect(() => {
// set our variable to true
let isApiSubscribed = true;
axios.get(API).then((res) => {
if (isApiSubscribed) {
// handle success
setData(res.data)
}
});
return () => {
// cancel the subscription
isApiSubscribed = false;
};
}, []);
But would like to ask, is it possible for the promise to be resolved in between the time when the component unmounts and cleanup is called, causing an error, that is, accessing the state that has been deallocated?
While I can't precisely tell you when useEffect's unsubscribe function is called more specifically than "when the component is unmounted", I can answer your question about the implications of when it is called:
is it possible for the promise to be resolved in between the time when the component unmounts and cleanup is called, causing an error, that is, accessing the state that has been deallocated?
In the code as written, it is possible for the Promise to resolve after the component unmounts. Your isApiSubscribed flag is a way of hiding the warning that might be displayed when this occurs in development mode, but it's not the correct way of dealing with this scenario.
When this happens when react is running in development mode, you might get a warning message like this:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
The point of this warning message is that you should take care to stop doing work that's no longer relevant after a particular component is unmounted. Not calling setData does prevent you from updating the state, but the work of the HTTP request will still always be completed, even if the component is unmounted. That's the bit that React is telling you that you should consider cancelling.
The "correct" way of altering this pattern to avoid spending unnecessary resources would be to stop the execution of the HTTP request when the component unmounts, which will cause the promise to reject:
useEffect(() => {
const abortController = new AbortController();
axios.get(API, { signal: abortController.signal }).then((res) => {
setData(res.data)
});
return () => {
abortController.abort();
};
}, []);
This ensures that not only do you not call setData on a component that's unmounted (which is a relatively trivial amount of work), but that you cancel the much larger piece of work which is a HTTP request (and associated response handling) when the component is unmounted.
It is theoretically possible that the following series of events could happen:
Task starts
Task completes
Promise resolves
Component unmounts
Next event tick triggers and the continuation passed to then() is called, calling setData(), triggering the warning message
But this requires precise timing and the spirit of the warning message is to prevent the much more common scenario:
Task starts
Component unmounts
Task completes
Promise resolves
Next event tick triggers

Must I specify unique Loading booleans when fetching data via multiple async functions within useEffects in React hooks to avoid concurrency problems?

My concern is if using the same isLoading boolean state can cause concurrency issues. Here's an example of what I'm talking about:
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
async function getWaitlistDetailsOnLoad() {
setIsLoading(true)
try {
const details = await getWaitlistDetailsForUser(
apiInstance,
product.id
);
if (details.status === 200) {
setWaitlistDetails(details.data);
}
} catch (e) {
console.log("Error getting waitlist details: ", e);
}
setIsLoading(false)
}
getWaitlistDetailsOnLoad();
}, [apiInstance, product.id]);
useEffect(() => {
async function getNextAvailableTimeOnLoad() {
setIsLoading(true)
try {
const time = await getNextAvailableTime(apiInstance, product.id);
if (time.status === 200) {
setNextAvailableTime(time.data);
}
setIsLoading(false);
} catch (e) {
console.log("Error getting next available time: ", e);
}
setIsLoading(false)
}
getNextAvailableTimeOnLoad();
}, [apiInstance, product.id]);
Of course I can just track two independent loading states like this:
const [firstLoader, setFirstLoader] = useState<boolean>(false);
const [secondLoader, setSecondLoader] = useState<boolean>(false);
...but if I don't have to, I'd rather not. It would make the code simpler and conditional rendering simpler as well for my use-case.
If you are guaranteed that the two asynchronous effects will not run at the same time (for instance, if OperationTwo, if one is called at the completion of OperationOne) then you could get away with using a single isLoading boolean, set as true at the start of OperationOne, and set as false at the completion of OperationTwo.
However, if you have two operations that might at any point run at the same time, then you should split them into two separate loaders, and use a single value that ORs them to determine the ultimate loading state of the view.
Let's consider a component that makes two asynchronous fetches on load:
const MyPretendComponent = () => {
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
MyService.fetchThatCompletesInOneSecond()
.then(() => setIsLoading(false));
MyService.fetchThatCompletesInTwentySeconds()
.then(() => setIsLoading(false));
}, [])
return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}
This should illustrate the problem nicely-- we have two asynchronous operations on component load-- one that completes in one second, and one that completes in twenty seconds. Both set isLoading as false when they complete. In this case, the first operation completes in one second and sets isLoading to false, and the UI erroneously reports that it is not in a loading state even though the second operation still has nineteen seconds left until it completes.
The cleaner version, using two booleans, is this:
const MyPretendComponent = () => {
const [isFirstOperationLoading, setIsFirstOperationLoading] = useState(false);
const [isSecondOperationLoading, setIsSecondOperationLoading] = useState(false);
useEffect(() => {
setIsFirstOperationLoading(true);
setIsSecondOperationLoading(true)
MyService.fetchThatCompletesInOneSecond()
.then(() => setIsFirstOperationLoading(false));
MyService.fetchThatCompletesInTwentySeconds()
.then(() => setIsSecondOperationLoading(false));
}, [])
const isLoading = isFirstOperationLoading || isSecondOperationLoading;
return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}
Here we've split the two loading states into their own discrete booleans. We still maintain a single isLoading boolean that simply ORs both the discrete booleans to determine if the overall loading state of the component is loading or not loading. Note that, in a real-life example, we would want to use much more semantically descriptive variable names than isFirstOperationLoading and isSecondOperationLoading.
In regards to splitting the two calls into separate useEffects: at a glance one might think that in splitting the two calls across two different useEffects you could mitigate or sidestep the issue of asynchronicity. However, if we walk through the component lifecycle, and think we will learn that this is not actually effective. Let's update our first example:
const MyPretendComponent = () => {
const [isFirstOperationLoading, setIsFirstOperationLoading] = useState(false);
const [isSecondOperationLoading, setIsSecondOperationLoading] = useState(false);
useEffect(() => {
setIsFirstOperationLoading(true);
setIsSecondOperationLoading(true)
MyService.fetchThatCompletesInOneSecond()
.then(() => setIsFirstOperationLoading(false));
MyService.fetchThatCompletesInTwentySeconds()
.then(() => setIsSecondOperationLoading(false));
}, [])
const isLoading = isFirstOperationLoading || isSecondOperationLoading;
return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}
The problem here is that the component does not run in this manner:
Run the first useEffect
When the first useEffect completes, run the second useEffect
When the second useEffect completes, render the UI
If it ran in that manner, we would be sitting and waiting for it to show the UI while it loaded.
Instead, the way it runs is like this:
Run the component render-- call any useEffects if applicable, then render the UI
Should any state updates occur (for instance, a state hook being called from the .then of a previous useEffect asynchronous call) then render again
Repeat step two every time a state or prop update occurs
Keeping this in mind, at every component render React will evaluate and run any and all applicable useEffect callbacks. It is not running them serially and tracking if one has completed-- such a thing would be difficult if not impossible. Instead, it is the developer's responsibility to track loading states and organize useEffect dependency arrays in such a way that your component state is logical and consistent.
So you see, separating the async calls by useEffect does not save us from dealing with potential concurrent async calls. In fact, if it did, it would mean that the UI load would be slower overall, because you would be serially calling two calls that could be called simultaneously. For instance, if both async calls took ten seconds each, calling them one after the other would mean it would take 20 seconds before loading would be completes, as opposed to running them at the same time and finishing loading after only ten seconds. It makes sense from a lifecycle perspective for the component to behave this way; we simply must code accordingly.
As #alexander-nied already pointed out there might by a bug in your code. useEffect callbacks can't be async. To use async/await nontheless you can wrap the three lines as follows and add an await statement.
// Version 1, closer to the original example
useEffect(() => {
async doSomething() {
await someAsyncMethod();
}
(async () => {
setIsLoading(true);
await doSomething();
setIsLoading(false);
})();
})
// Version 2, closer to the current example
useEffect(() => {
async doSomething() {
setIsLoading(true);
await someAsyncMethod();
setIsLoading(false);
}
doSomething();
})
To answer your question:
It certainly depends on you usecase, but I would recommend against just using one single boolean, since it allows for weired scenarios to occur.
What if one request is way faster than the other? Your loading indicator would be set to false, instead of informing the user that there is still work being done in the background.

How to use the react hook "useMemo" with asynchronous functions?

How can I await, to resolve/reject, the promise returned by a function wrapped in the react hook "useMemo"?
Currently, my code looks like this:
// Get the persisted email/username
const persistedUsername = useMemo(async () => {
let username;
try {
username = await AsyncStorage.getData(`#${ASYNC_STORAGE_KEY}:username`);
} catch {}
return username;
}, []);
EDIT
What I am trying to achieve is to get the data before the component is rendered, some kind of "componentWillMount" life-cycle. My two options were:
Computing the value using useMemo hook to avoid unnecessary recomputations.
Combine useEffect + useState. Not the best idea because useEffect runs after the component paints.
#DennisVash has proposed the solution to this problem in the comments:
Blocking all the visual effects using the useLayoutEffect hook (some kind of componentDidMount/componentDidUpdate) where the code runs immediately after the DOM has been updated, but before the browser has had a chance to "paint" those changes.
As you can see, persistedUsername is still a promise (I am not waiting the result of the asynchronous function)...
Any ideas? Is it not a good idea to perform asynchronous jobs with this hook? Any custom hook?
Also, what are the disadvantages of performing this operation in this way compared to using useEffect and useState?
Same thing with useEffect and useState:
useEffect(() => {
// Get the persisted email/username
(async () => {
const persistedUsername = await AsyncStorage.getData(
`#${ASYNC_STORAGE_KEY}:username`
);
emailOrUsernameInput.current.setText(persistedUsername);
})();
}, []);
Thank you.
Seems like the question is about how to use componentWillMount with hooks which is almost equivalent to useLayoutEffect (since componentWillMount is deprecated).
For more additional info, you should NOT resolve async action in useMemo since you will block the thread (and JS is single-threaded).
Meaning, you will wait until the promise will be resolved, and then it will continue with computing the component.
On other hand, async hooks like useEffect is a better option since it won't block it.

calling setState method with other statements

I know that calling setState method means I don’t have to manually invoke the ReactDOM.render method. Below is some example code:
...
render() {
return (
<button onClick={this.handleClick}>
Click
</button>
)
}
handleClick = () => {
this.setState({ counter: this.state.counter + 1 }, () => this.setState({ hasButtonBeenClicked: this.state.counter > 0 }));
this.props.callback();
}
since there is another statement this.props.callback(); below the this.setState() method, so does ReactDOM.render method get called before or after this.props.callback();?
since there is another statement this.props.callback(); below the this.setState() method, so does ReactDOM.render method get called before or after this.props.callback();?
After, in your case. The state update and call to render is asynchronous if setState is called within a React event handler (and may be asynchronous even if not, more here). The sequence (within a React event handler) is:
You call setState with the state update.
You call this.props.callback();
React processes the state update (possibly combining it with other pending updates).
(Some handwaving here about shouldComponentUpdate.)
React calls render to have it render the current state.
If you want this.props.callback(); called after the new state has been rendered, put it in a function as the second argument of setState:
handleClick = () => {
this.setState(
{ counter: this.state.counter + 1 },
() => {
this.props.callback();
}
);
}
or
handleClick = () => {
this.setState(
{ counter: this.state.counter + 1 },
this.props.callback
);
}
...if this.props.callback doesn't care what this you call it with.
More here:
State Updates May Be Asynchronous (that title drives me nuts, because it's not that they may be asynchronous, it's that they are asynchronous)
setState API docs
If you look at my example of your code, this.props.callback() gets called immediately after the state has been updated.
handleClick = () => {
this.setState({ counter: this.state.counter + 1, hasButtonBeenClicked: true }, () => this.props.callback() );
}
You have chained setStates, which seem unnecessary for readability. These should be grouped into a single setState call automatically, however one is nested into the callback and this would trigger multiple re-renders.. depending on ShouldComponentUpdate.
To avoid guessing or leaving it susceptible to future React updates:
Using the setState callback for this.props.callback, is the best way to ensure it is executed after setState completes.
Updated, based on T.J Crowders feedback and research with event handlers:
The way your code was structured it is most probable that this.props.callback will be called prior to the setState actually completing state updates, it will trigger the re-render once state updates.
-Because, it is in an asynchronous call and within a React event handler. SetState should update state after.(I am still no expert on promises, and have to wonder if there is a chance state could update within the millisecond, depending on your browsers internal clock and the batch processing)
For clarity, to others. This example is asynchronous and that means the code continues to be executed, while waiting for resolution. While setState returns a promise. In this case, it should absolutely continue to process this.props.callback(), until the promise is resolved.

How to fix logging issues when setting state in componentWillMount

I am having issue with state as i'm not 100% i'm using componentDidMount and componentWillMount correctly.
I have set the constructor and super props, and I am getting a user object with the getCurrentUser() method and setting the user state with the new object.
componentWillMount() {
const current_user = getCurrentUser().then(user => {
this.setState({
user: user
});
console.log(user);
});
}
componentDidMount() {
console.log(this.state.user);
}
It logs the user correctly in componentWillMount, but logs an empty object in componentDidMount.
Any guidance would be massively appreciated!
Simply don't use componentWillMount,
Do it in componentDidMount.
In practice, componentDidMount is the best place to put calls to fetch data, for two reasons:
Using DidMount makes it clear that data won’t be loaded until after the initial render. This reminds you to set up initial state properly, so you don’t end up with undefined state that causes errors.
If you ever need to render your app on the server (SSR/isomorphic/other buzzwords), componentWillMount will actually be called twice – once on the server, and again on the client – which is probably not what you want. Putting the data loading code in componentDidMount will ensure that data is only fetched from the client.
getCurrentUser is an asynchronous method which calls another asynchronous method (setState).
I am pretty sure that you will first see the log entry from componentDidMount and only afterwards the log entry from componentWillMount.
What's happening is:
React calls componentWillMount
You start an async call (getCurrentUser)
componentWillMount returns immediatelly, without waiting for the promise to complete
React calls componentDidMount
Your promise resolves
The logs are due to the asynchronous nature of your method getCurrentUser. When you call getCurrentUser in componentWillMount, it might result in an output after the componentDidMount has finished executing and hence you see the initial state in componentDidMount. However the console.log in componentWillMount is in the getCurrentUser .then promise callback which will log the current value received from getCurrentUser()

Categories

Resources