Unexpected behaviour using React useEffect hook - reactjs

I started studying reaction hooks. But I have some doubts. I thought I understood, but the empirical evidence shows unexpected behavior, at least as far as I believe I know.
To illustrate my perplexities or misunderstandings, I take the classic exercise that implements a simple clock as an example.
Below is the code that implements the functional component.
import {useState, useEffect} from 'react';
const Clock = (props) => {
const {show, country, timezone} = props;
const t = Date.now () + 3600 * timezone * 1000;
const dateInitial = new date (t);
const [datE, setDate] = useState (dateInitial);
useEffect (() => {
const interval = setInterval (() => {
const t = date.getTime () + (1 * 1000);
setDate (new date (t));
}, 1000);
return () => {
// ONLY clears the return of this function
// When the component is unmounted from the DOM
console.log ("Cleanup function (componentWillUnmount ())");
clearTimeout (interval); // The setTimeout is cleared as soon as the component is unmounted from the DOM
}
}, [date);
return (
<h2 style = {{display: show? true: 'none'}}> Today in {country} is {date.toLocaleDateString () + '' + date.toLocaleTimeString ()} </h2>
)
};
export default Clock;
The functional component uses useEffect hooked to the "date" state property.
The function:
() => {
const t = date.getTime () + (1 * 1000);
setDate (new date (t));
}, 1000);
Passed as the first parameter to useEffect, it should be used for componentDidMount and componentDidUpdate life cycle events.
While the cleanup function:
return () => {
// ONLY clears the return of this function
// When the component is unmounted from the DOM
console.log ("Cleanup function (componentWillUnmount ())");
clearTimeout (interval); // The setTimeout is cleared as soon as the component is unmounted from the DOM
}
it should be executed at the componentWillUnmount event of the component lifecycle.
But if I "run" our component, I see this in the logs:
As you can see, the componentWillUnmount event occurred every second. If I'm not wrong, shouldn't the componentWillUnmount happen when the component is removed from the DOM? In this circumstance, we only have a new rendering of the DOM and not the removal of the element from the DOM. I am perplexed. On the other hand, if you try the following example with the class component:
Clock with class component (on codepen.io)
You will see that console.log ("componentWillUnmount"); never appears ...
I also observe that the component that I inserted in the post loses several seconds if I change the tab, while with the one on codepen.io it does not lose a "beat".
Excuse me, I'm a little confused.
I thank you in advance for your always precious clarifications.

useEffect represent componentDidMount and componentWillUnmount only if you give it an empty dependency array:
useEffect(() => {
//code that will only be executed when component mount
return () => {
//code that will only executed when component unmount
}
}, [])
In your example, you have date in the dependency array, this mean, every-time date changes, a new execution, think about it like that useEffect is a mini component which it's life depends on the date value every-time date changes it unmout and remount.
The reason we need the return function in such case, is when we have for example a listener that depend on the date value, so each time the date chnage we need to remove the old listener with old value and attach new listener with new value.
useEffect(() => {
//We attach a listener which depends on date value
const listenerHandler = () => {console.log(date)}
window.addEventListener('event', listenerHandler)
return () => {
//We need to remove the listener with old value so we don't have many listeners attached each time date changes
window.removeEventListener('event', listenerHandler)
}
}, [date])

Related

Why is the updated state variable not shown in the browser console?

I understand that the method returned by useState is asynchronous, however, when I run the this code, I am delaying the console.log by upto 5 seconds, but it still logs the previous value and not the updated value of the state variable. The updated value would be 2, but it still logs 1. In the react developer tools however, I can see the state changing as I press the button, though I am curious to know why after even such a delay the console prints an obsolete value? This is not the case with class components and setState but with function components and useState.
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [variable, setVariable] = useState(1);
const handleClick = () => {
setVariable(2);
setTimeout(() => {
console.log(variable);
}, 2000);
};
return <button onClick={handleClick}>Button</button>;
}
export default App;
In your code your setTimeout is getting the variable value from the closure at the time it was invoked and the callback function to the setTimeout was created. Check this GitHub issue for the detailed explanation.
In the same issue, they talk about utilizing useRef to do what you are attempting. This article by Dan Abramov packages this into a convenient useInterval hook.
State updates are asynchronous. That means, that in order to view the new value, you need to log It on the next render using useEffect and adding it to the dependencies array:
In this example, give a look at the order the logs appear:
First, you will have the current one, and once triggered, you will have the new value, and then it will become the 'old value' until triggered again.
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => { console.log(`new state rolled: ${counter}`);
}, [counter]);
console.log(`Before rolling new State value: ${counter}`);
const handleClick = () => setCounter(counter++)
return <button onClick={handleClick}>Button</button>;
}
export default App;
Another technic to console.log a value afterward a state change is to attach a callback to the setState:
setCounter(counter++, ()=> console.log(counter));
I hope it helps.
A state take some time to update. The proper way to log state when it updates, is to use the useEffect hook.
setTimeout attaches the timer and wait for that time, but it will keep the value of variable from the beginning of the timer, witch is 1
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [variable, setVariable] = useState(1);
const handleClick = () => {
setVariable(2);
};
useEffect(() => {
console.log(variable);
}, [variable]);
return <button onClick={handleClick}>Button</button>;
}
export default App;
This is not the case with class components and setState but with
function components and useState
In class components, React keep the state in this.state & then call the Component.render() method whenever its need to update due to a setState or prop change.
Its something like this,
// pseudocode ( Somewhere in React code )
const app = MyClassComponent();
app.render();
// user invoke a callback which trigger a setState,
app.setState(10);
// Then React will replace & call render(),
this.state = 10;
app.render();
Even though you cannot do this.state = 'whatever new value', React does that internally with class components to save the latest state value. Then react can call the render() method and render method will receive the latest state value from this.state
So, if you use a setTimeout in a class component,
setTimeout(() => {
console.log(this.state) // this render the latest value because React replace the value of `this.state` with latest one
}, 2000)
However in functional component, the behaviour is little bit different, Every time when component need to re render, React will call the component again, And you can think the functional components are like the render() method of class components.
// pseudocode ( Somewhere in React code )
// initial render
const app = MyFuctionalComponent();
// state update trigger and need to update. React will call your component again to build the new element tree.
const app2 = MyFunctionalComponent();
The variable value in app is 1 & variable value in app2 is 2.
Note: variable is just a classic variable which returned by a function that hooked to the component ( The value save to the variable is the value return by the hook when the component was rendering so it is not like this.state i.e its hold the value which was there when the component is rendering but not the latest value )
Therefore according to the Clouser, at the time your setTimeout callback invoke ( Which was called from app ) it should log 1.
How you can log the latest value ?
you can use useEffect which getting invoke once a render phase of a component is finished. Since the render phase is completed ( that mean the local state variables holds the new state values ) & variable changed your console log will log the current value.
useEffect(() => {
console.log(variable);
}, [variable])
If you need the behaviour you have in class components, you can try useRef hook. useRef is an object which holds the latest value just like this.state but notice that updating the value of useRef doesn't trigger a state update.
const ref = useRef(0);
const handleClick = () => {
setVariable(2); // still need to setVariable to trigger state update
ref.current = 2 // track the latest state value in ref as well.
setTimeout(() => {
console.log(ref.current); // you will log the latest value
}, 2000);
};

Getting updated Value from React Context

I have an app which uses a global Time Value. All components need to use the same time value.
I have put this time value, named system_time, in context and want all components to be able to get the latest system_time from context.
How can I ensure that the context.system_time is updated every second and the updated system_time is available to all components whenever required?
Use a mixture of useState and useEffect. Update state with setInterval, which you will set to 1000 milliseconds. Then, just make sure you clear the interval when you unmount.
You might use something similar to this:
import React, { useState, useEffect } from 'react'
const App = () => {
const [date, setDate] = useState()
useEffect(() => {
let interval = setInterval(() => setDate(Date.now()), 1000);
//clear on unmount
return () => {
clearInterval(interval)
}
})
return (
<div>
{date}
</div>
)
}
export default App
Your return will look different, of course. But I just used that to demonstrate that it updates every second.
Then, just pass your state to your components.

React component state wiped before component unmounted

If I return a function from useEffect I can be sure that that function will run when a component unmounts. But React seems to wipe the local state before it calls my unmounting function.
Consider this:
function Component () {
const [setting, setSetting] = useState(false)
useEffect(() => {
setSetting(true)
// This should be called when unmounting component
return () => {
console.log('Send setting to server before component is unmounted')
console.log(setting) // false (expecting setting to be true)
}
}, [])
return (
<p>Setting is: {setting ? 'true' : 'false'}</p>
)
}
Can anyone confirm that the expected behaviour is that the components state should be wiped? And, if that is the correct behaviour, how does one go about firing off the current component state to a server just before the component is unmounted?
To give some context, I'm debouncing a post request to a server in order to avoid firing it every time the user changes a setting. The debouncing works nicely, but I need a way to fire the request once a user navigates away from the page, as the queued debouncing method will no longer fire from the unmounted component.
It's not that React "wipes out the state value", it's that you have closure on setting value (the value on-mount).
To get expected behavior you should use a ref and another useEffect to keep it up to date.
function Component() {
const [setting, setSetting] = useState(false);
const settingRef = useRef(setting);
// Keep the value up to date
// Use a ref to sync the value with component's life time
useEffect(() => {
settingRef.current = setting;
}, [setting])
// Execute a callback on unmount.
// No closure on state value.
useEffect(() => {
const setting = settingRef.current;
return () => {
console.log(setting);
};
}, []);
return <p>Setting is: {setting ? "true" : "false"}</p>;
}

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/

How to rerender component in useEffect Hook

Ok so:
useEffect(() => {
}, [props.lang]);
What should I do inside useEffect to rerender component every time with props.lang change?
Think of your useEffect as a mix of componentDidMount, componentDidUpdate, and componentWillUnmount, as stated in the React documentation.
To behave like componentDidMount, you would need to set your useEffect like this:
useEffect(() => console.log('mounted'), []);
The first argument is a callback that will be fired based on the second argument, which is an array of values. If any of the values in that second argument change, the callback function you defined inside your useEffect will be fired.
In the example I'm showing, however, I'm passing an empty array as my second argument, and that will never be changed, so the callback function will be called once when the component mounts.
That kind of summarizes useEffect. If instead of an empty value, you have an argument, like in your case:
useEffect(() => {
}, [props.lang]);
That means that every time props.lang changes, your callback function will be called. The useEffect will not rerender your component really, unless you're managing some state inside that callback function that could fire a re-render.
UPDATE:
If you want to fire a re-render, your render function needs to have a state that you are updating in your useEffect.
For example, in here, the render function starts by showing English as the default language and in my use effect I change that language after 3 seconds, so the render is re-rendered and starts showing "spanish".
function App() {
const [lang, setLang] = useState("english");
useEffect(() => {
setTimeout(() => {
setLang("spanish");
}, 3000);
}, []);
return (
<div className="App">
<h1>Lang:</h1>
<p>{lang}</p>
</div>
);
}
Full code:
Simplest way
Add a dummy state you can toggle to always initiate a re-render.
const [rerender, setRerender] = useState(false);
useEffect(()=>{
...
setRerender(!rerender);
}, []);
And this will ensure a re-render, since components always re-render on state change.
You can call setRerender(!rerender) anywhere anytime to initiate re-render.
const [state, set] = useState(0);
useEffect(() => {
fn();
},[state])
function fn() {
setTimeout((), {
set(prev => prev + 1)
}, 3000)
}
The code above will re-render the fn function once every 3 seconds.

Resources