Related to weird behaviour of react useState and useEffect - javascript

import {useEffect,useState} from 'react';
export default function App() {
const [count,setCount]=useState(0);
const [flag,setFlag]=useState(false);
function increment(){
setCount(prevState=>{
if(flag)
return prevState
return prevState+1;
});
}
useEffect(function(){
increment();
setFlag(true);
increment();
},[]);
return (
<div className="App">
{count}
</div>
);
}
Was playing around with effects and states in reatct functional component, I expected the code to output "1" but it's giving the output as "2", Why is it happening and How can I make it print 1 ?

Once you call setFlag, React will update the returned value of your useState call [flag,_] = useState() on the next render.
Your setFlag(true) call schedules a re-render, it doesn't immediately update values in your function.
Your flag is a boolean const after all -- it can't be any value but one value in that function call.
How to solve it gets interesting; you could put the the flag inside of a single state object i.e. useState({count: 0, flag: false})
But more likely, this is an academic problem. A count increment sounds like something that would trigger on a user interaction like a click, and so long as one function doesn't call increment() multiple times (this sounds unusual), the re-render will happen in time to update your flag state.

For performance reasons, React defers useState hook updates until function completes its execution, i.e. run all statements in the function body and then update the component state, so React delays the update process until a later time.
Thus, when increment function execution is completed, React updates the state of count. But for setFlag method, the execution environment is a context of useEffect hook's callback, so here React's still waiting for a completion of useEffect's callback function. Therefore, inside the callback of useEffect the value of flag is still false.
Then you again called your increment function and when this function finished its execution, your count again was incremented by 1.
So, in your case, the key factor is the way of deferring state updates until function execution by React.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
React Component: setState()
For more information, you can also read about Batch updates in React (especially in React 18) or Reactive programming (this is not React), where the main idea is real-time or timely updates.

For a better understanding I would think it of as replacing the invocation directly with setter and we know how the state batching works so ...
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
useEffect(function () {
// increment(); becomes below
setCount((prevState) => {
if (flag) return prevState;
return prevState + 1;
});
// queued update count returns
// count => count + 1 0 0 + 1 = 1
setFlag(true);
//set flag=true in next render
// increment(); becomes below
setCount((prevState) => {
if (flag) return prevState;
return prevState + 1;
});
// so flag is still false here and count is 1
// queued update count returns
// count => count + 1 1 1 + 1 = 2
// done and count for next render is 2 and flag will be false
}, []);
return <div className="App">{count}</div>;
A better explaination in Docs - Queueing state updates and state as snapshot

State updates are "batched". See the other answers for an explanation. Here's a workaround using useRef - since a ref can be updated during this render, you can use it like a "normal" variable.
const { useState, useRef, useEffect } = React;
function App() {
const [count, setCount] = useState(0);
const flag = useRef(false);
function increment() {
setCount(prevState => {
if (flag.current)
return prevState;
return prevState + 1;
});
}
useEffect(function() {
increment();
flag.current = true;
increment();
}, []);
return <div className="App">{count}</div>;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Related

With flushSync() inside useEffect do we do the same as using useLayoutEffect?

https://reactjs.org/docs/react-dom.html#flushsync
Force React to flush any updates inside the provided callback
synchronously. This ensures that the DOM is updated immediately.
// Force this state update to be synchronous.
flushSync(() => {
setCount(count + 1);
});
// By this point, DOM is updated.
Knowing that, it is the same as using useLayoutEffect, or do I misunderstand flushSync()?
const App = () => {
const [name, setName] = React.useState("Leonardo");
React.useEffect(() => {
ReactDOM.flushSync(() => {
for (let index = 1; index <= 100000; index++) { // for simulate blocking
console.log(index);
}
setName("Jose");
});
});
return (
<div>
<h1>Hello {name}</h1>
</div>
);
};
¿it is the same that this?
React.useLayoutEffect(() => {
for (let index = 1; index <= 100000; index++) {
console.log(index);
}
setName("Jose");
});
useLayoutEffect is useful for things that need to happen before paint your dom or when your code is causing flickering. it's already synchronous and executed always before every useEffect hook in your code.
flushSync is used to convert the setState into synchronous. in 99% of cases you will use flushSync inside a handler like a form submit handler, outside of useEffect to execute an imperative action
function handleSubmit(values) {
flushSync(() => {
setForm(values);
});
}
Be aware that flushSync force a re-rendering, so use it carefully
The common use case of flushSync is update the DOM after settings the state immediately. example scroll to the new added element in the list
flushSync(() => {
setElements((elements) => [
...elements,
{
id: 'random',
},
]);
});
// scroll to element here
Check this example https://codesandbox.io/s/react-18-batching-updates-flushsync-forked-vlrbq8. you can delete flushSync and see the diff
flushSync is used to force React to flush a state update and when you try to put it inside useEffect it won't affect when useEffect is invoked, it will always be after the changes have been reflected on the browser, whereas useLayoutEffect is invoked before and this is the main difference between them.
so flushSync is not a function that is supposed to be executed inside useEffect you will even get this warning
Warning: flushSync was called from inside a lifecycle method. React cannot flush when React is already rendered. Consider moving this call to a scheduler task or microtask.
They are not same. Your code might give the same performance, functional, and results, yet they are different in nature.
flushSync is a low-level API that flushes updates to the React DOM immediately, bypassing the normal scheduling mechanism. It should be used sparingly and only when necessary, as it can lead to poor performance and is more prone to bugs and inconsistencies. On the other hand, useLayoutEffect is a higher-level hook that schedules a DOM update to happen synchronously after all other updates have been processed. It is generally the preferred way to ensure that updates are synchronized with the layout of the page.
I did this code to see if I understood everything, please correct me:
function App2() {
const [c, setC] = React.useState(0);
const inc1 = () => {
/*setC is asynchronous (withoutFlushSync), so it continues the
code downwards and therefore from the DOM it
brings us the value without increment in 1*/
setC((c) => c + 1);
console.log(document.getElementById("myId").innerText); // old value from DOM
console.log(c); // However below log will still point to old value ***BECAUSE OF CLOSURE***
};
const inc2 = () => {
/*waits until the DOM is modified with the new state (in the first click
c = 1)*/
ReactDOM.flushSync(() => { // Wait
setC((c) => c + 1);
});
/* brings the c = 1 of the DOM because it waited until the DOM
was modified with the new state incremented by one. */
console.log(document.getElementById("myId").innerText); // new value from DOM
console.log(c); // However below log will still point to old value ***BECAUSE OF CLOSURE***
};
return (
<div className="App">
Count: <div id="myId">{c}</div>
<button onClick={inc1}>without flushSync</button>
<button onClick={inc2}>with flushSync</button>
</div>
);
}

Having some troubles getting useEffect, useCallback and setTimeout to work together

Trying to implement some custom scroll behaviour for a slider based on the width. It's using useState, to keep track of the current and total pages inside the slider. This is the code I have ended up with trying to acomplise it but it has some unexpected behaviour.
const [isTimeoutLocked, setIsTimeoutLocked] = useState(false)
const handleScroll = useCallback(() => {
const gridContainer = document.querySelector(".grid");
const totalPages = Math.ceil(
gridContainer.scrollWidth / gridContainer.clientWidth + -0.1
);
setTotalPages(totalPages);
const scrollPos = gridContainer.clientWidth + gridContainer.scrollLeft + 2;
if (gridContainer.scrollWidth > scrollPos) {
gridContainer.scrollBy({
left: gridContainer.clientWidth + 20.5,
behavior: "auto",
block: "center",
inline: "center",
});
setCurrentPage(currentPage + 1);
setIsTimeoutLocked(false)
} else {
gridContainer.scrollTo(0, 0);
setCurrentPage(1);
setIsTimeoutLocked(false)
}
},[currentPage]);
useEffect(() => {
if(!isTimeoutLocked){
setTimeout(() => {
setIsTimeoutLocked(true)
document.querySelector(".grid") && handleScroll();
}, 5000);
}
}, [currentPage, displayDate, isTimeoutLocked, handleScroll]);
The problem here is that when the currentPage is displayed in the ui it will reset back to one if the else runs inside the handleScroll function which is totally fine but then it will result back to the previous value instead of going back to 2 when it gets to the next page. Also since I have added the isTimeoutLocked as a dependency to the useEffect since it was asking for it the setTimeout will run more often but I only want to have it as a dependency to get the correct value not so the useEffect runs everytime it changes. The purpose of the isTimeoutLocked is so when you change the content inside the container by changing the current day it has not registered a bunch of timeouts.
You should add a cleanup function to your useEffect. Since you mutate isTimeoutLocked in your useEffect it can force multiple setTimeouts to run and that is probably the result of wierd behaviour.
When using setTimeout inside useEffect it is always recommended to use it with cleanup. Cleanup will run before the next useEffect is triggered, that gives you a chance to bail out of next setTimeout triggering. Code for it is this:
useEffect(() => {
if(!isTimeoutLocked){
const tid = setTimeout(() => {
setIsTimeoutLocked(true)
document.querySelector(".grid") && handleScroll();
}, 5000);
// this is the cleanup function that is run before next useEffect
return () => clearTimeout(tid);
}
You can find more information here: https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup .
Another thing to be careful is the async nature of setState and batching. Since you are using setState inside a setTimeout, React will NOT BATCH them, so EVERY setState inside your handleScroll will cause a rerender (if it was not inside a setTimeout/async function React would batch them). Since you have alot of interconected states you should look into useReducer.

React Hooks useEffect to call a prop callback from useCallback

I'm trying to make a general-purpose infinite scroller with React Hooks (and the ResearchGate React Intersection Observer). The idea is that a parent will pass down a mapped JSX array of data and a callback that will asynchronously get more data for that array, and when the intersection observer fires because you've scrolled down enough to reveal the loading icon, the callback gets called and more data is loaded.
It works well enough, except one thing: esLint tells me that because I'm calling the getMore function (from the props) inside a useEffect, it must be a dependency of that effect. But because in the parent's callback I'm accessing its data array's length, that array must be a dependency of useCallback there. And then that callback modifies the array.
TL;DR: I'm getting race conditions that cause the async callback to trigger multiple times when it shouldn't, because the callback function reference is changing and then being passed down to the thing that's calling it.
Here's some code to clarify.
The callback in the parent:
const loadData = useCallback(async () => {
if (hasMore) {
const startAmount = posts.length;
for (let i = 0; i < 20; ++i) {
posts.push(`I am post number ${i + startAmount}.`);
await delay(100);
}
setPosts([...posts]);
setHasMore(posts.length < 100);
}
}, [posts, hasMore]);
posts and hasMore are just state variables, with posts being passed down as the data array in props to the child. That function is being passed to the child in props, which has this (getMore is the destructured prop for the callback, isLoading is just a boolean state variable):
useEffect(() => {
if (isLoading) {
(async () => {
await getMore();
setIsLoading(false);
})();
}
}, [isLoading, getMore]);
I'm setting isLoading to true to trigger the effect; but it's also triggering because getMore's reference changes when the parent loads data and the function memoizes. That shouldn't happen. I could just disable esLint for that line, but I assume there's a better solution, and I'd like to know what it is.
Solution: don't use useEffect at all. Just call the loading function directly from the observer and have that set isLoading and call the callback rather than having isLoading trigger the callback.
const loadData = async (observerEntry) => {
if (observerEntry.isIntersecting && !disabled && !isLoading) {
setIsLoading(true);
await getMore();
setIsLoading(false);
}
};

React Hooks: State always setting back to initial value

Switching over to react hooks and using them for the first time. My state always seems to be set back to the initial value I pass it (0). Code is to have a page automatically scroll down and up. The page is just practice to displaying various file types. What happens is the scrollDir variable will switch to being set to either 1 or -1 and 0. So the console will display 1,0,1,0,1,0,1,0 etc... How do I get the state to stay during an update?
function App(props) {
const [scrollDir, setScrollDir] = useState(0);
function scrollDown() {
if(document.documentElement.scrollTop < 10)
{
setScrollDir(1);
}
else if(document.documentElement.scrollTop >= document.documentElement.scrollHeight - window.innerHeight)
{
setScrollDir(-1);
}
window.scrollBy(0, scrollDir);
}
useEffect(() => {
setInterval(scrollDown, 100);
});
return (
<StackGrid monitorImagesLoaded={true} columnWidth={"33.33%"} >
<img src={posterPNG} />
<img src={posterJPG} />
<img src={posterGIF} />
<video src={posterMP4} loop autoPlay muted />
<Document file={posterPDF}>
<Page pageNumber={1} />
</Document>
</StackGrid>
);
}
useEffect hook takes second argument, an array.
If some value in that array changes then useEffect hook will run again
If you leave that array empty useEfect will run only when component mount - (basicaly like ComponentDidMount lifecycle method)
And if you omit that array useEffect will run every time component rerenders
For example:
useEffect runs only once, when component mounts:
useEffect(() => {
//code
},[]);
useEffect runs everytime when component rerenders:
useEffect(() => {
//code
});
useEffect runs only when some of these variables changes:
let a = 1;
let b = 2;
useEffect(() => {
//code
},[a,b]);
Also, if you set return statement in useEffect hook, it has to return a function, and that function will always run before useEffect render
useEffect(() => {
// code
return () => {
console.log("You will se this before useEffect hook render");
};
}, []);
Using setInterval with hooks isn't very intuitive. See here for an example: https://stackoverflow.com/a/53990887/3984987. For a more in-depth explanation you can read this https://overreacted.io/making-setinterval-declarative-with-react-hooks.
This useInterval custom hook is pretty easy to use as well https://github.com/donavon/use-interval. (It's not mine - I ran across this after responding earlier)

Reactjs-setState previous state is the first argument, props as the second argument

I have read in this official article these lines:
this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
Can anyone please explain to me what the following code is trying to achieve by giving an example.
this.setState((prevState, props) => ({
couter: prevState.counter + props.increment
}));
I am referring to this official website of reactjs React.js
They say you should do like that instead of the below example.
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
They can't assure the state will have the correct value if you access like this because setState() will happen asynchronously, other updates could occur and change the value. If you are going to calculate the state based on the previous state, you have to make sure you have the last and most up to date value, so they made setState() accept a function that is called with prevState and props, so you can have the correct value to update your state, like the example below.
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
To add to Bruno's answer, the correct function above is called a pure function. React is big on something called immutability which means that every declared value should never be changed from its original declaration if possible. The variables in that function aren't your actual props and state until you pass them in, which means on the javascript function stack (the thread that queues up sync and async calls) the values and references to properties will be stored differently, creating uncertainty of what the value will be in the "wrong" case.
Updating state based on the previous state
Suppose the age is 42. This handler calls setAge(age + 1) three times:
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
However, after one click, the age will only be 43 rather than 45! This is because calling the set function does not update the age state variable in the already running code. So each setAge(age + 1) call becomes setAge(43).
To solve this problem, you may pass an updater function to setAge instead of the next state:
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
Here, a => a + 1 is your updater function. It takes the pending state and calculates the next state from it.
React puts your updater functions in a queue. Then, during the next render, it will call them in the same order:
a => a + 1 will receive 42 as the pending state and return 43 as the next state.
a => a + 1 will receive 43 as the pending state and return 44 as the next state.
a => a + 1 will receive 44 as the pending state and return 45 as the next state.
There are no other queued updates, so React will store 45 as the current state in the end.
By convention, it’s common to name the pending state argument for the first letter of the state variable name, like a for age. However, you may also call it like prevAge or something else that you find clearer.
React may call your updaters twice in development to verify that they are pure.
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
And from https://reactjs.org/docs/react-component.html#setstate:
setState() enqueues changes to the component state and tells React
that this component and its children need to be re-rendered with the
updated state.
Think of setState() as a request rather than an immediate command
to update the component. For better perceived performance, React may
delay it, and then update several components in a single pass. React
does not guarantee that the state changes are applied immediately.
Understanding with a example
This concept may be hard to understand and specially why it could cause issues, so I wrote an example that show an error happening:
/* Imagine props and states is the same as this.props and this.state */
var state = { counter: 0 } ; var props = { } ;
/* Our fake implementation of react setState */
var setStatesToRun = []
function setState(myFunction) { setStatesToRun.push(myFunction); }
/* Our fake implementation of react batch update */
function batchRunStateUpdates() {
propsLocal = props
stateLocal = state
f1 = setStatesToRun.pop()
newState = f1(stateLocal, propsLocal) // Will run increment by 3
console.log(newState) // newState: { counter: 3 }
console.log(state) // state: { counter: 0 }
f2 = setStatesToRun.pop()
newState = f2(newState, propsLocal) // Will run increment by 2
console.log(newState) // newState: { counter: 2 }
console.log(state) // state: { counter: 0 }
// ... get the next setState function loop
console.log("Will update global state")
state = newState
console.log(state) // state: { counter: 2 } // WRONG!
}
console.log(setStatesToRun) // []
// Right
setState((prevState, props) => { counter: prevState.counter + 3 });
// WRONG, using state (this.state)
setState((prevState, props) => { counter: state.counter + 2 });
console.log(setStatesToRun) // [func, func]
batchRunStateUpdates();
At the top we have some fake versions of React's methods. Those are overly simplified, but help explain what happens.
Then, we use setState the same way we do in React. One usage is right, the other is wrong.
Notice the final global state should be state: { counter: 5 }, but because of how we didn't respect React's recommendations we got state: { counter: 2 }
You can play with this code in https://jsfiddle.net/oniltonmaciel/g96op3sy/

Categories