React Hooks: setState behaving strange inside useEffect - reactjs

I have a component, which I'd like to keep the scroll position each time it re-renders.
Therefore I have created a useState for it's position. Inside the useEffect, I've created an intervall, which will track the current position every x seconds and set it using setState.
It seems to be working fine to this point, because when I console log the state (scrollTop) inside the useEffect, I get the updated numbers.
I keep track of the element by setting it as a ref with useRef.
const [scrollState, setScrollState] = useState(0);
const element = useRef<HTMLDivElement>(null);
useEffect(() => {
console.log(scrollState);
const captureElementScroll = setInterval(() => {
if (element.current) {
const scrollTop = _.get(element.current, "scrollY");
setScrollState(scrollTop);
}
}, 2000);
if (element.current && element.current) {
element.current.scrollTo({
behavior: "smooth",
top: scrollState
});
}
return () => {
clearInterval(captureElementScroll);
};
}, [scrollState]);
Now the problem is, each time this component gets re-rendered, the current state is set back to zero.
Maybe a problem with the parent components? I'm out of ideas. Am I doing it the correct way?

Now looking back to this issue (I have discontinued what I was working on ), I'm pretty sure that I captured the scroll position on the wrong level.
Like I guessed already, the problem was that the Parent component got re-rendered and therefore the state got lost.
Maybe I'll work on something similar at some point. If so I'll write a more detailed answer.

Related

React renders without actually changing the state when setInterval is used

I'm going to introduce my question in two steps with slightly different code blocks in both.
Step 1:
Below we have a React application which renders itself every two seconds and therefore causes the browser to print render to the console. If the user presses any key, the renders will stop which in turn stops the console prints. Please ignore the line commented out for now.
import { useState, useEffect, useRef } from 'react';
function App() {
const [number, setUpdate] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const intervalRef = useRef(undefined);
useEffect(() => {
intervalRef.current = setInterval(() => setUpdate(prevNumber => ++prevNumber), 2000);
window.addEventListener('keydown', handleKeyDown);
}, []);
const handleKeyDown = () => {
clearInterval(intervalRef.current);
console.log('console log here');
// setIsPaused(isPaused);
};
console.log('render');
return null;
};
export default App;
Here is a screenshot of the application:
What has happened above, is that I've let the component render five times and then I've pressed a key to stop the component from rendering.
Step 2:
In step 2 we have exactly the same application with the exception of not commenting out the state set in handleKeyDown.
const handleKeyDown = () => {
clearInterval(intervalRef.current);
console.log('console log here');
// This is no longer commented out. Why does it cause a new render?
setIsPaused(isPaused);
};
Here is a screenshot of the application with the code change made in step 2:
Again, I've let the component to render five times after I've pressed a key. But now there is an extra render even though the state should not be changing (because the state is not actually mutating because we set the same value as was already in the state) by setIsPaused(isPaused).
I'm having difficulty to understand what might the reason to cause the extra render at step 2. Maybe it's something obvious?
setIsPaused(isPaused) never causes a new render if I comment out the other state change which is run by setInterval which makes me even more baffled.
This is a known quirk, see #17474. It’s a side effect introduced by the new concurrent mode. The component function did re-run, but DOM will remain untampered.
I also found people post this interesting example. You can try exp with the code. The component function contains something like <div>{Math.random()}</div> although random number did changed in that extra re-run of the function, it wouldn’t reflect onto DOM if state isn’t changed.
Conclusion. You can consider this side effect harmless most of time.
U̶p̶d̶a̶t̶i̶n̶g̶ ̶a̶ ̶s̶t̶a̶t̶e̶ ̶n̶e̶v̶e̶r̶ ̶m̶e̶a̶n̶s̶ ̶D̶O̶M̶ ̶w̶i̶l̶l̶ ̶r̶e̶r̶e̶n̶d̶e̶r̶,̶ ̶y̶o̶u̶ ̶a̶r̶e̶ ̶r̶i̶g̶h̶t̶ ̶̶s̶e̶t̶I̶s̶P̶a̶u̶s̶e̶d̶(̶i̶s̶P̶a̶u̶s̶e̶d̶)̶̶ ̶w̶i̶l̶l̶ ̶n̶o̶t̶ ̶r̶e̶r̶e̶n̶d̶e̶r̶ ̶t̶h̶e̶ ̶D̶O̶M̶,̶ ̶b̶u̶t̶ ̶i̶t̶ ̶w̶i̶l̶l̶ ̶s̶e̶t̶ ̶t̶h̶e̶ ̶s̶t̶a̶t̶e̶ ̶a̶n̶d̶ ̶w̶i̶l̶l̶ ̶g̶i̶v̶e̶ ̶y̶o̶u̶ ̶t̶h̶e̶ ̶u̶p̶d̶a̶t̶e̶d̶ ̶v̶a̶l̶u̶e̶.̶ ̶(̶I̶ ̶a̶m̶ ̶c̶o̶n̶s̶i̶d̶e̶r̶i̶n̶g̶ ̶o̶n̶l̶y̶ ̶s̶t̶a̶t̶e̶ ̶w̶i̶t̶h̶ ̶f̶i̶e̶l̶d̶ ̶̶i̶s̶P̶a̶u̶s̶e̶d̶̶ ̶n̶o̶t̶h̶i̶n̶g̶ ̶e̶l̶s̶e̶)̶
Update: I did know this behavior existed until reading the accepted answer.

React useState and useEffect nesting. Is this okay?

I'm playing with React hooks, and I'm trying to make a very basic clock. It's working fine and dandy, even the profiler says there are no weird rerenders going on. The part that worries me a bit is where useEffect calls updateTimeAndDate, which changes the state. I feel like it should go into a rerender loop. Why does it still work? I can't put it any better, sorry. :D
Could it also be a bit nicer? Here it is:
const addZero = (trunk) => (trunk >= 10 ? "" : "0") + trunk;
const [hours, setHours] = useState(addZero(0));
const [minutes, setMinutes] = useState(addZero(0));
let timeAndDate;
function updateTimeAndDate() {
timeAndDate = new Date();
setHours(addZero(timeAndDate.getHours()));
setMinutes(addZero(timeAndDate.getMinutes()));
}
useEffect(() => {
updateTimeAndDate();
});
setInterval(updateTimeAndDate, 500);
I put useEffect there to update the time immediately after loading the page, instead of waiting for a long long half second.
There are a couple issues that you will encounter with the code you've showed:
You will run an infinitely loop that causes your component to rapidly re-render. This is because you are calling setInterval on every render, and by calling updateTimeAndDate within setInterval, you are updating state, which in turns causes the component to re-render.
You have not specified a dependency array in your useEffect, so it will run on every re-render, further amplifying the infinite loop problem.
A possible alternative would be to only call the useEffect once by specifying an empty dependency array. You can also clear the interval when your component unmounts (by specifying a return value in your useEffect).
Here's a codesandbox demonstrating: https://codesandbox.io/s/stack-overflow-set-interval-89spq
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const addZero = trunk => (trunk >= 10 ? "" : "0") + trunk;
const [hours, setHours] = useState(addZero(0));
const [minutes, setMinutes] = useState(addZero(0));
const [seconds, setSeconds] = useState(addZero(0));
let timeAndDate;
function updateTimeAndDate() {
timeAndDate = new Date();
setHours(addZero(timeAndDate.getHours()));
setMinutes(addZero(timeAndDate.getMinutes()));
setSeconds(addZero(timeAndDate.getSeconds()));
}
useEffect(() => {
const interval = setInterval(updateTimeAndDate, 1000);
return () => clearInterval(interval);
}, []);
// setInterval(updateTimeAndDate, 500);
return (
<div className="App">
<h1>{hours}</h1>
<h2>{minutes}</h2>
<h3>{seconds}</h3>
</div>
);
}
To answer your questions
Is it ok?
Calling updateTimeAndDate (which updates the state) inside useEffect is ok. Nevertheless, in your current code you will face issues with interval as #RobertCooper mentioned.
However, I disagree with Robert on one thing. And it is that you will fall in the infinite loop. This leads us to your second question:
I feel like it should go into a rerender loop. Why does it still work?
Your current code does not fall into infinite loop due to the way React handles effects.
According to the documentation:
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects.
Therefore, as you update the state with same hour and same minute React does not fire your effect. Hence, there is no infinite loop in this particular case.

Save React Component scroll position in Redux on Component update

I have a React Component using a hook to save the scroll position of the component when the component unmounts. This works great but fails when navigating from one set of data to another set of data without the component unmounting.
For instance, imagine the Slack Interface where there is a sidebar of message channels on the left and on the right is a list of messages (messageList). If you were to navigate between two channels, the messageList component would update with a new set of data for the messageList, but the component was never unmounted so scroll position never gets saved.
I came up with a solution that works, but also throws a warning.
My current useEffect hook for the component (stripped down) and the code that currently saves scroll position whenever the messageList ID changes:
// Component...
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
// Save scroll position when Component unmounts
useEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, []);
// Save scroll position when Parent ID changes
const oldParent = usePrevious(parent);
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
// ...Component
The error this throws is:
Warning: Cannot update a component from inside the function body of a different component.
And the line that is causing it is the setScrollOffset call inside of the last if block. I'm assuming that while this works it is not the way that I should be handling this sort of thing. What is a better way to handle saving scroll position when a specific prop on the component changes?
Add parent._id to the dependency array. Refactor your code to still cache the previous parent id, add that to the dependency, and move the conditional test inside the effect.
Cleaning up an effect
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.
// Return previous parent id and cache current
const oldParent = usePrevious(parent);
// Save scroll position when Component unmounts or parent id changes
useEffect(() => {
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id, oldParent]);
If this does't quite fit the bill, then use two effects, one for the mount/unmount and the other for just updates on the parent id.
Thanks to the suggestions of #drew-reese, he got me pointed down the right path. After adopting his solution (which previously I could not get working properly), I was able to isolate my problem to usage with react-router. (connected-react-router in my case). The issue was that the component was rendering and firing the onScroll event handler and overwriting my scroll position before I could read it.
For me the solution ended up being to keep my existing useEffect hook but pull the scroll offset save out of it and into useLayoutEffect (Had to keep useEffect since there is other stuff in useEffect that I removed for the sake of keeping the sample code above lean). useLayoutEffect allowed me to read the current scroll position before the component fired the onScroll event which was ultimately overwriting my saved scroll position reference to 0.
This actually made my code much cleaner overall by removing the need for my usePrevious hook entirely. My useLayoutEffect hook now looks like this:
useLayoutEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id]);

React hooks update sort of whenever they feel like it

So, I'm using hooks to manage the state of a set of forms, set up like so:
const [fieldValues, setFieldValues] = useState({}) // Nothing, at first
When setting the value, the state doesn't update:
const handleSetValues = values => {
const _fieldValues = {
...fieldValues,
...values
}
setFieldValues(_fieldValues)
console.log(fieldValues) // these won't be updated
setTimeout(() => {
console.log(fieldValues) // after ten seconds, it's still not updated
},10000)
}
If I call the function a second time, it'll have updated, but that's not gonna work for me.
I never saw behaviour like this in class components.
Is it meant to... like, not update? Or just update whenever it feels like it? Really confusing behaviour.
setFieldValues(_fieldValues) is an async call, means you won't able to get the result in the very next line of this.
You can use useEffect hook.
useEffect(() => {
// do your work here
}, [fieldValues]);
It seems from your question that you have background of Class components of React, so useEffect is similar to componentDidMount and componentDidUpdate lifecycle methods.
useEffect calls whenever the state in the dependency array (in your case [fieldValues]) changes and you get the updated value in useEffect body.
You can also perform componentWillUnmount work in useEffect as well.
Have a brief guide.
setFieldValues is an asynchronous function, so logging the value below the statement will not have any effect.
Regarding using setTimeout, the function would capture the current value of props being passed to it and hence that would be the value printed to the console. This is true to any JS function, see the snippet below:
function init(val) {
setTimeout(() => {
console.log(val);
}, 1000);
}
let counterVal = 1;
init(counterVal);
counterVal++;
So how can we print the values when the value changes? The easy mechanism is to use a useEffect:
useEffect(() => {
console.log(fieldValues)
}, [fieldValues]);

Can not update state inside setInterval in react hook

I want to update state every second inside setinterval() but it doesn't work.
I am new to react hook so can not understand why this is happening.
Please take a look at the following code snippet and give me advice.
// State definition
const [gamePlayTime, setGamePlayTime] = React.useState(100);
let targetShowTime = 3;
.........................
// call function
React.useEffect(() => {
gameStart();
}, []);
.............
const gameStart = () => {
gameStartInternal = setInterval(() => {
console.log(gamePlayTime); //always prints 100
if (gamePlayTime % targetShowTime === 0) {
//can not get inside here
const random = (Math.floor(Math.random() * 10000) % wp("70")) + wp("10");
const targetPosition = { x: random, y: hp("90") };
const spinInfoData = getspinArray()[Math.floor(Math.random() * 10) % 4];
NewSpinShow(targetPosition, spinInfoData, spinSpeed);
}
setGamePlayTime(gamePlayTime - 1);
}, 1000);
};
The reason why you did not get updated state is because you called it inside
useEffect(() => {}, []) which is only called just once.
useEffect(() => {}, []) works just like componentDidMount().
When gameStart function is called, gamePlaytime is 100, and inside gameStart, it uses the same value however the timer works and the actual gamePlayTime is changed.
In this case, you should monitor the change of gamePlayTime using useEffect.
...
useEffect(() => {
if (gamePlayTime % targetShowTime === 0) {
const random = (Math.floor(Math.random() * 10000) % wp("70")) + wp("10");
const targetPosition = { x: random, y: hp("90") };
const spinInfoData = getspinArray()[Math.floor(Math.random() * 10) % 4];
NewSpinShow(targetPosition, spinInfoData, spinSpeed);
}
}, [gamePlayTime]);
const gameStart = () => {
gameStartInternal = setInterval(() => {
setGamePlayTime(t => t-1);
}, 1000);
};
...
You're creating a closure because gameStart() "captures" the value of gamePlayTime once when the useEffect hook runs and never updates after that.
To get around this, you must use the functional update pattern of React hook state updating. Instead of passing a new value directly to setGamePlayTime(), you pass it a function and that function receives the old state value when it executes and returns a new value to update with. e.g.:
setGamePlayTime((oldValue) => {
const someNewValue = oldValue + 1;
return someNewValue;
});
Try this (essentially just wrapping the contents of your setInterval function with a functional state update):
const [gamePlayTime, setGamePlayTime] = React.useState(100);
let targetShowTime = 3;
// call function
React.useEffect(() => {
gameStart();
}, []);
const gameStart = () => {
gameStartInternal = setInterval(() => {
setGamePlayTime((oldGamePlayTime) => {
console.log(oldGamePlayTime); // will print previous gamePlayTime value
if (oldGamePlayTime % targetShowTime === 0) {
const random = (Math.floor(Math.random() * 10000) % wp("70")) + wp("10");
const targetPosition = { x: random, y: hp("90") };
const spinInfoData = getspinArray()[Math.floor(Math.random() * 10) % 4];
NewSpinShow(targetPosition, spinInfoData, spinSpeed);
}
return oldGamePlayTime - 1;
});
}, 1000);
};
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Dan Abramov article explains well how to work with hooks, state, and the setInterval() type of API!
Dan Abramov! Is one of the React maintaining team! So known and I personally love him!
Quick explanation
The problem is the problem of how to access the state with a useEffect() that executes only once (first render)!
Note: For a deep explanation of why the state isn't updated within the useEffect callback and other inside useEffect callbacks. Check the last section about closures and react re-render ...
The short answer is: by the use of refs (useRef)! And another useEffect() that run again when update is necessary! Or at each render!
Let me explain! And check the Dan Abramov solution! And you'll get better the statement above at the end! With a second example that is not about setInterval()!
=>
useEffect() either run once only, or run in each render! or when the dependency update (when provided)!
Accessing state can be possible only through a useEffect() that run and render each relevant time!
Or through setState((state/*here the state*/) => <newStateExpression>)
But if you want to access the state inside useEffect() => re-run is necessary! meaning passing and executing the new callback!
That doesn't work well with setInterval! If you clear it and re-set it each time! the counter get reset! Leading to no execution if the component is re-rendering fast! And make no sense!
If you render only once! The state is not updated! As the first run, run a one callback! And make a closure! The state is fixed! useEffect(() => { <run once, state will stay the same> setInterval(() => { <state fixed as closure of that time> }) }, []).
For all such kind of situation! We need to use useRef! (refs)!
Save to it a callback that holds the state! From a useEffect() that rerenders each time! Or by saving the state value itself in the ref! Depending on the usage!
Dan abramov solution for setInterval (simple and clean)
That's what you are looking for!
useInteval hook (by Dan Abramov)
import React, { useState, useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Usage
import React, { useState, useEffect, useRef } from 'react';
function Counter() {
let [count, setCount] = useState(0);
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
We can see how he kept saving the new callback at each re-render! A callback that contains the new state!
To use! it's a clean simple hook! That's a beauty!
Make sure to read Dan article! As he explained and tackled a lot of things!
setState()
Dan Abramov mentioned this in his article!
If we need to set the state! Within a setInteral! One can use simply setState() with the callback version!
useState(() => {
setInterval(() => {
setState((state/*we have the latest state*/) => {
// read and use state
return <newStateExpression>;
})
  }, 1000);
}, []) // run only once
we can even use that! Even when we are not setting a state! Possible! Not good though! We just return the same state value!
setState((state) => {
// Run right away!
// access latest state
return state; // same value (state didn't change)
});
However this will make different react internal code part run (1,2,3), And checks! Which ends by bailing out from re-rendering! Just fun to know!
We use this only when we are updating the state! If not! Then we need to use refs!
Another example: useState() with getter version
To showcase the how to work with refs and state access! Let's go for another example! Here is another pattern! Passing state in callbacks!
import React from 'react';
function useState(defaultVal) {
// getting the state
const [state, setState] = React.useState(defaultValue);
// state holding ref
const stateRef = React.useRef();
stateRef.current = state; // setting directly here!
// Because we need to return things at the end of the hook execution
// not an effect
// getter
function getState() {
// returning the ref (not the state directly)
// So getter can be used any where!
return stateRef.current;
}
return [state, setState, getState];
}
The example is of the same category! But here no effect!
However, we can use the above hook to access the state in the hook simply as below!
const [state, useState, getState] = useState(); // Our version! not react
// ref is already updated by this call
React.useEffect(() => {
setInteval(() => {
const state = getState();
// do what you want with the state!
// it works because of the ref! Get State to return a value to the same ref!
// which is already updated
}, 1000)
}, []); // running only once
For setInterval()! The good solution is Dan Abramov hook! Making a strong custom hook for a thing is a cool thing to do! This second example is more to showcase the usage and importance of refs, in such state access need or problem!
It's simple! We can always make a custom hook! Use refs! And update the state in ref! Or a callback that holds the new state! Depending on usage! We set the ref on the render (directly in the custom hook [the block execute in render()])! Or in a useEffect()! That re-run at each render or depending on the dependencies!
Note about useEffect() and refs setting
To note about useEffect()
useEffect => useEffect runs asynchronously, and after a render is painted on the screen.
You cause a render somehow (change state, or the parent re-renders)
React renders your component (calls it)
The screen is visually updated
THEN useEffect runs
Very important of a thing! useEffect() runs after render() finishes and the screen is visually updated! It runs last! You should be aware!
Generally, however! Effects should be run on useEffect()! And so any custom hook will be ok! As its useEffect() will run after painting and before any other in render useEffect()! If not! As like needing to run something in the render directly! Then you should just pass the state directly! Some people may pass a callback! Imagine some Logic component! And a getState callback was passed to it! Not a good practice!
And if you do something somewhere of some such sense! And talking about ref! Make sure the refs are updated right! And before!
But generally, you'll never have a problem! If you do then it's a smell! the way you are trying to go with is high probably not the right good way!
Closure and more. Why the state variables are not having the latest value?
The why you can't simply access the state value directly boils down to the closure notion. Render function at every re-render go totally with a new call. Each call has its closures. And at every re-render time, the useEffect callback is a new anonymous function. With its new scope of value.
And here the dependency array matter. To access the closure of this call. And so the recent state of this call. You have to make useEffect use the new callback. If dependencies change then that would happen. If not that wouldn't.
And if you do [] then useEffect() would run the first time only. every new call after the first render. Would always have useEffect get a new anonymous function. But none of them is effective or used (run).
The same concept applies to useCallback and many other hooks. And all of that is a result of closures.
callbacks inside the useEffect callback
ex: event listners, setInterval, setTimeout, some(() => {}), api.run(() => {})
Now even if you update the useEffect callback through dependency change. And let's say you did some event listener or setInterval call. But you did it conditionally so if already running no more setting it. The setInterval callback or event listener callback wouldn't get access to the recent state value. Why? you already guessed. They were created in the first run, first call space and all the state values are closure passed down at that **time**. And in the later updates and calls. It's a new render function call. And a whole different useEffect callback as well. Sure it's the same function code. But not same functions at all. The initial callback of the initial run on that render function call. Is a function that was created back then. And then the event listener callback or the setInterval callback was created back then. And they are still in memory and referred to. The event listener part of the event listener API objects instances, and the object that made the registration and the setInterval part of the node runtime. They had the state closure having the value of the time. And never get to see or know about any other re-render call. **Unless somehow you. inject something within. That still referencesorcan access the latest value(references or globals). And those values come from the hooks (useState) and their internal machinery. All it would have is theclosures of the time of the creation.**
Funny metaphor
Most people falling into this trap. Look at the code and saying hey why when the state updates it's not getting the newer value. And the answer is. The state value is something coming from useState, it's not a global variable. And even though you are looking at the same code. The first call and later calls are all different spaces (beings). The only thing making it possible for us to work with functions in this sense is the hooks. And how they store the state and bring it back.
And a good metaphor would be: to get to an office and do some contract and deal. And then later come back and enter the same office, but wait not really the same office rather one that looks the same. But a new office took its place (moved out, moved in, same activity). And you wonder why they didn't recognize you. Yup a bit of an off of a metaphor. But still good a bit.
On the whole I hope that gave you a good sense!
You shouldn't use setInterval with hooks. Take a look at what Dan Abramov, one of the maintainers of React.js, said regarding an alternative on his blog: https://overreacted.io/making-setinterval-declarative-with-react-hooks/

Resources