Introduction
...
Info |
---|
I was putting together some code to demonstrate how modifying a state variable causes a component to rerender. All was going will until I put a timer on the component, that got the current time and displayed it in the page. That worked fine example 1, but it all changed when I wanted to keep a list of all the times and print them all out each time a new time was added, things a got a little weird, example 2. |
Example 1.
Get the time and continually display it
Code Block | ||
---|---|---|
| ||
import React, { useState } from 'react'
import { useEffect } from 'react';
function getTheTime()
{
let currentDate = new Date();
let currentTime = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds();
return currentTime;
}
export default function FCDisplayTimeInSameSpot(){
console.log("Executing FCDisplayTimeInSameSpot()");
const [containerState, setContainerState] = useState(["<NO TIME SET>"]);
const getDisplayData = () => {
const cdata = containerState
return cdata;
}
// This useEffect will only run once when the component is first mounted
useEffect(()=>{
setInterval(()=>{
setContainerState(getTheTime());
}, 10000);
}, [])
// This useEffect will run every time the component re-renders
useEffect(() =>{
console.log(getDisplayData())
})
// This useEffect will only run once when the component is first mounted
useEffect(() =>{
console.log("mounting the FCDisplayTimeInSameSpot");
}, []);
// This useEffect will run every time the component is unmounted
useEffect(() =>{
return ()=>console.log("unmounting the FCDisplayTimeInSameSpot");
});
return (
<>
<p>Time is - {getDisplayData()}</p>
</>
)
} |
Drop this component as a child of any other component to see it work e.g.
Code Block | ||
---|---|---|
| ||
import React from 'react';
import './App.css';
import FCDisplayTimeInSameSpot from './FCDisplayTimeInSameSpot';
function App() {
const switchComponents = true;
return (
<div className="App">
<header className="App-header">
<FCDisplayTimeInSameSpot/>
</header>
</div>
);
}
export default App;
|
Monitor the behaviour from the outputs in the browser’s console, you will notice the following
Event | What’s executed in order shown | ||||
---|---|---|---|---|---|
Initial loading of the component Notice that both these useEffects() with the lambda and empty array are called once and only once |
| ||||
Component needs to be re-rendered Notice that the component method is first called, then it is unmounted, then is finally re-rendered through the component return() block (you can see this because the useEffect() with only the lambda is called |
|
Notice that the component method is always called. In actual fact, the useState() methods are called each time the component method is called, therefore resetting the state variable back to its initial value. To see this at work, modify the useEffect() call at 22-26 so the following
Code Block | ||
---|---|---|
| ||
// This useEffect will only run once when the component is first mounted
useEffect(()=>{
setInterval(()=>{
const timeStamp = containerState;
setContainerState(getTheTime());
}, 30000);
}, []) |
The timeStamp
variable serves no real purpose except to allow us to put a breakpoint in the code and examine the values in the variables. I've also changed the timer from 10 seconds to 30 seconds to give us time to examine the variables.
Set a breakpoint on the timeStamp variable. When the code breaks, hover your mouse over timeStamp
it should be undefined. Step over it and it should be set to <NO TIME SET>
. This is the initial value from the useState()
call.
So what are we seeing here? Whenever the component’s state changes (achieved by modifying any of its state variables through their setter methods), the component method will be called and useState()
will be called re-initialising the variable. So we need to make use of the useEffect()
method during the component's lifecycle.
If you want data to persist across component renders, you need to use useRef()
. We'll cover this in another section
Example 2
All of the above explains why the following code will not run as expected if you don’t use the useRef() callback
Code Block | ||
---|---|---|
| ||
import React, { useState } from 'react'
import { useEffect } from 'react';
import { useRef } from 'react';
function getTheTime()
{
let currentDate = new Date();
let currentTime = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds();
return currentTime;
}
export default function ContainerFcWuE(){
const [simpleField, setSimpleField] = useState('');
const [containerState, setContainerState] = useState([getTheTime()]);
/* Without these two lines below, this code will not work as expected
* The problem is, every time a state field changes the function is
* is re-executed in it's entirety, so the state fields are completely
* reset. I know I was suprised by this fact!
*
* useRef allows you to create a mutable value that does not cause a
* re-render when it is changed.
*
* If you remove containerRef and containerRef.current and replace
* containerRef.current with containerState, you will see the problem.
*
* A description of this problem and a solution can be found at
* https://stackoverflow.com/questions/56511176/state-being-reset
*/
const containerRef = useRef([]);
containerRef.current = containerState;
const addToList = name => {
console.log(`Current array: ${containerRef.current}`);
const update = [...containerRef.current, name];
console.log(`Updated array: ${update}`);
setContainerState(update);
}
const addTimeToList = () => {
addToList(getTheTime())
};
const getDisplayData = () => {
const cdata = containerState.map((item, index) =>
<li key={index}>{item}</li>);
return cdata;
}
// This useEffect will only run once when the compone is mounted
useEffect(()=>{
setInterval(()=>{
addTimeToList()
}, 10000);
}, [])
// This useEffect will run every time the component re-renders
useEffect(() =>{
console.log(getDisplayData())
})
// This useEffect will run every time the simpleField changes
useEffect(() =>{
console.log(simpleField);
}, [simpleField]);
// This useEffect will run every time the component is unmounted
useEffect(() =>{
return ()=>console.log("unmounting the ContainerFcWuE");
});
return (
<>
Type something (open dev tools/console to see behaviour)<input type='text' value={simpleField} onChange={(e) => setSimpleField(e.target.value)} />
<div onClick={e => setSimpleField("YYY")}>
<h2>[fcwue] Click Me</h2>
</div>
<ul>{getDisplayData()}</ul>
</>
)
} |