React: Re-render strange behavior - reactjs

Example:
https://codesandbox.io/s/friendly-lumiere-srnyu?file=/src/App.js
I found the console is print 3 times and the last time was state:2,version:1, but the view is keep state:2,version:0.
I can't understand what happen in React.
I thought that "React would not call re-render if set the same value (primitive value)", But this example overthrew my idea.
The initial value of v is true (line 12), it will call third re-render if call setV(true)(line 16), But not update on the View in this re-render call.
Just call 1 times if comment lines 8 to 10.
import React, { useEffect, useState } from "react";
let version = 0;
export default () => {
const [state, setState] = useState(1);
// useEffect(() => {
// setState(2);
// }, []);
const [v, setV] = useState(true);
useEffect(() => {
setTimeout(() => {
version = 1;
setV(true);
}, 1000);
}, []);
console.log(`state:${state},version:${version}`);
return (
<div>
{`state:${state},version:${version}`}
</div>
);
};

version is not part of the state, the state being what React uses to decide if it needs to re-render a component.

Related

How to use useEffect/state/variables properly without user interaction?

My goal is to set up a game loop but a simple test isn't working as expected. In the following component, I am trying the useEffect hook to increment food. I expect to see "Food: 1". Instead I see "Food: 0". When I inspect the component with the dev tools, I can see that food is 2. I've discovered that the component mounts, increments food, unmounts, mounts again and increments food once more.
I have two questions:
Can I do something about the double mount? (like prevent it or wait until the final mount with a nested component perhaps?)
Why does the displayed food count still equal zero? Is it because game inside <span>Food: {game.food}</span> still refers to the initial instance? If so, how do I get the latest instance?
Component:
import React from "react";
class Game {
food = 0;
}
export default function App() {
const [game, setGame] = React.useState(new Game());
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return game;
});
});
return <span>Food: {game.food}</span>;
}
Don't Mutate State Objects
React uses reference comparisons and expects the reference of the root state object to change if any data within it has changed.
For Example:
// DON'T
setGame((game) => {
// mutate and return same object
game.food += 1;
return game;
});
// DO
setGame((current) => {
// create new object with updated food value
return {
...current,
food: current.food + 1
};
});
Using the same reference will cause components to not update as expected.
useEffect Dependency Array
A useEffect without a dependency array will trigger every time the component renders.
If you wish for the useEffect to only trigger on mount provide an empty dependency array.
For Example:
// Every Render
useEffect(() => {
alert('I trigger every render');
});
// On Mount
useEffect(() => {
alert('I trigger on mount');
}, []);
// Everytime the reference for game changes
useEffect(() => {
alert('I trigger everytime the game state is update');
}, [game]);
Conclusion
"Mount twice" probably you are using react 18 and have strict mode enabled. It will trigger useEffect twice in dev mode from docs
If you want to update the view, you should make the reference of the game variable changes (instead of changing its attrs).
Solution
const initialGame = {
food: 0
}
export default function App() {
const [game, setGame] = React.useState(initialGame);
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return {...game};
});
}, []);
return <span>Food: {game.food}</span>;
}
No you should not useEffect as a loop, its execution depends on your component states and its parent component, so this leaves 3 solutions 1st while loop, 2nd requestAnimationFrame and 3rd setInterval. while loop is discouraged because it will block event loop and canceling/stopping can be tedious.
double mount ? i think its react double checking function, which does this only dev mode. Once you switch to requestAnimationFrame you won't be having that issue.
use tried mutate state and react doesn't recognizes this so it doesn't re render. solution: return new object.
updating states
useEffect(() => {
setGame((current) => {
const newState = { ...current, food: current.food + 1 }
return newState
})
}, [])
using setInterval to act as loop
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000)
return () => clearInterval(id)
}, [])
using requestAnimationFrame to act as loop
// credit: https://css-tricks.com/using-requestanimationframe-with-react-hooks/
const requestRef = React.useRef()
const animate = (time) => {
setCount((count) => count + 1)
requestRef.current = requestAnimationFrame(animate)
}
useEffect(() => {
requestRef.current = requestAnimationFrame(animate)
return () => cancelAnimationFrame(requestRef.current)
}, []) // Make sure the effect runs only once

Why is my useState variable initialized every time my React components is rendered?

While working on some custom hooks in React I have observed the following behavior: Every time my component renders, my useState variables are initialized. More specifically, I have observed this by using an auxiliary function to dynamically create data for testing.
In order to demonstrate this behavior of React, I have created a super simple "TestComponent" (please refer to the code below). Every time my component renders, it logs "testData called!" in the console.
What did I do wrong? How can I prevent React from calling my "test-data-generating-function" testData every time my component renders?
PS: I tried to wrap my function testData into a useCallback hook. But that does not work since useCallback cannot be used at "top level". Also, instead of finding a "workaround", I would really like to understand the root cause of my problem.
import React, { useEffect, useState } from 'react';
const testData = (): number[] => {
console.log('testData called!');
const returnValues: number[] = [];
for (let i = 1; i <= 10; i++) {
returnValues.push(i);
}
return returnValues;
};
const TestComponent = () => {
// useState hooks
const [data, setData] = useState<number[]>(testData());
const [counter, setCounter] = useState<number>(1);
// useEffect hook
useEffect(() => {
const increaseCounter = () => {
setCounter((counter) => counter + 1);
setTimeout(() => increaseCounter(), 1000);
};
increaseCounter();
}, []);
return (
<div>
{data.map((item) => (
<p key={item}>{item}</p>
))}
<p>{counter}</p>
</div>
);
};
export default TestComponent;
I believe what happens here is that your function to retrieve the data (testData()) is being called for each render, but not necessarily updates the state.
A react component is essentialy a function, and for each render the whole function is executed (including the call to testData()).
If you use the callback signature of useState, it will be called only on the first render:
const [data, setData] = useState(() => testData());

Is it okay to change boolean which is a hook and dependency of useEffect in React?

I am working on CSS Transition on React that it automatically unmount after 2 seconds. I am thinking about using useEffect and useState to solve this problem.
I know that changing dependencies inside useEffect causes infinite loop.
For example, the code below will cause infinite loop.
const [count, setCount] = useState(0);
useEffect(() => {
setCount(prev => prev + 1);
},[count]);
But I think infinite loop won't happen if I set dependency to boolean and set if statement inside useEffect just like the code below.
const [showStatus, setshowStatus] = useState(false);
useEffect(() => {
const timeId = setTimeout(() => {
if (showStatus === true){
setshowStatus(false)
}
}, 2000)
return (() => {clearTimeout(timeId)})
}, [showStatus]);
I am relatively new to React so I am worried about this code. Do I have any problems using this code?
I think it's answered here better
React hooks - right way to clear timeouts and intervals
import { useState, useEffect } from "react";
const delay = 5;
export default function App() {
const [show, setShow] = useState(false);
useEffect(
() => {
let timer1 = setTimeout(() => setShow(true), delay * 1000);
// this will clear Timeout
// when component unmount like in willComponentUnmount
// and show will not change to true
return () => {
clearTimeout(timer1);
};
},
// useEffect will run only one time with empty []
// if you pass a value to array,
// like this - [data]
// than clearTimeout will run every time
// this value changes (useEffect re-run)
[]
);
return show ? (
<div>show is true, {delay}seconds passed</div>
) : (
<div>show is false, wait {delay}seconds</div>
);
}

Some insight on React behavior would be appreciated. Issue with custom hook

I have a small react example that is puzzling me.
You can run the code at codesanbox
The code is very simple, there is a component TestComponent that displays 'wowzers', and calls a custom hook useCustomHook.
useCustomHook does three things. It declares a useState [list, set_list], then a useEffect based on list to inform when there is a change, and then a 1 second timer that will update the list.
What I expected was that the useEffect would initially run and then a second later it would run again when the list is updated. However, it updates every second. Not only this, but the useCustomHook is being re-entered from TestComponent starting the process all over again, and this happens repeatedly.
Can someone explain please why, when the list is updated with set_list in the timer callback that this causes TestComponent to call useCustomHook again (and again)
I thought I understood that principles of using react and have developed numerous but small applications. This simple example is really throwing me off. Any help would be appreciated.
The code in index.js is a follows.
import { useState, useEffect } from "react";
import React from "react";
import ReactDOM from "react-dom";
const useCustomHook = () => {
const [list, set_list] = useState([]);
useEffect(() => console.log("useEffect", "list updated"), [list]);
setTimeout(() => set_list(() => []), 1000);
};
const TestComponent = () => {
const hook = useCustomHook();
return <span>wowzers</span>;
};
class App extends React.Component {
render() {
return <TestComponent />;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
setTimeout is right in the hook body... each time the hook is called it runs the entire body. The timeout is enqueueing a state update which triggers a rerender. Rerenders run all the hooks again.
const useCustomHook = () => {
const [list, set_list] = useState([]);
useEffect(() => console.log("useEffect", "list updated"), [list]);
setTimeout(() => set_list(() => []), 1000); // <-- unintentional side-effect!
};
What I expected was that the useEffect would initially run and then a
second later it would run again when the list is updated.
Place the setTimeout in a mounting useEffect, i.e. empty dependency array, so it runs exactly once after the initial mounting render cycle.
const useCustomHook = () => {
const [list, set_list] = useState([]);
useEffect(() => console.log("useEffect", "list updated"), [list]);
useEffect(() => {
setTimeout(() => set_list(() => []), 1000);
}, []);
};
might skiped some step and not exactly how it gos, surely it will keep updating and the sequenced steps can help you some
import { useState, useEffect } from "react";
import React from "react";
import ReactDOM from "react-dom";
// called every render, because function component is called every render
const useCustomHook = () => {
// 4 first useState, list=[] by default, alloc memory X
// 9,15... useState, list=[] changed by set_list, loaded from memory X
const [list, set_list] = useState([]);
// 5 register effect, callback called every time list change
// 10,16... effect alread registered, skip
useEffect(() =>
// 12,18... list updated because []!==[]
console.log("useEffect", "list updated"), [list]);
// 6,11,17... settimeout, callback in 1 sec
setTimeout(() =>
// 7,13,19... update memory X, triggering rerender
set_list(() => []), 1000);
};
// function component is called every render
const TestComponent = () => {
// 3,8,14...
const hook = useCustomHook();
return <span>wowzers</span>;
};
class App extends React.Component {
render() {
return <TestComponent />; //2
}
}
ReactDOM.render(<App />, document.getElementById("root")); // 1

How to deal with stale state values inside of a useEffect closure?

The following example is of a Timer component that has a button (to start the timer), and two tags that display the number of elapsed seconds, and the number of elapsed seconds times 2.
However, it does not work (CodeSandbox Demo)
The Code
import React, { useState, useEffect } from "react";
const Timer = () => {
const [doubleSeconds, setDoubleSeconds] = useState(0);
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
console.log("Creating Interval");
setSeconds((prev) => prev + 1);
setDoubleSeconds(seconds * 2);
}, 1000);
} else {
clearInterval(interval);
}
return () => {
console.log("Destroying Interval");
clearInterval(interval);
};
}, [isActive]);
return (
<div className="app">
<button onClick={() => setIsActive((prev) => !prev)} type="button">
{isActive ? "Pause Timer" : "Play Timer"}
</button>
<h3>Seconds: {seconds}</h3>
<h3>Seconds x2: {doubleSeconds}</h3>
</div>
);
};
export { Timer as default };
The Problem
Inside the useEffect call, the "seconds" value will always be equal to the its value when the useEffect block was last rendered (when isActive last changed). This will result in the setDoubleSeconds(seconds * 2) statement to fail. The React Hooks ESLint plugin gives me a warning regarding this problem that reads:
React Hook useEffect has a missing dependency: 'seconds'. Either include it or remove the dependency array. You can also replace
multiple useState variables with useReducer if 'setDoubleSeconds'
needs the current value of 'seconds'.
(react-hooks/exhaustive-deps)eslint
And correctly so, adding "seconds" to the dependency array (and changing setDoubleSeconds(seconds * 2) to setDoubleSeconds((seconds + 1) * ) will render the correct results. However, this has a nasty side effect of causing the interval to be created and destroyed on every render (the console.log("Destroying Interval") fires on every render).
So now I am looking at the other recommendation from the ESLint warning "You can also replace multiple useState variables with useReducer if 'setDoubleSeconds' needs the current value of 'seconds'".
I do not understand this recommendation. If I create a reducer and use it like so:
import React, { useState, useEffect, useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "SET": {
return action.seconds;
}
default: {
return state;
}
}
};
const Timer = () => {
const [doubleSeconds, dispatch] = useReducer(reducer, 0);
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
console.log("Creating Interval");
setSeconds((prev) => prev + 1);
dispatch({ type: "SET", seconds });
}, 1000);
} else {
clearInterval(interval);
}
return () => {
console.log("Destroying Interval");
clearInterval(interval);
};
}, [isActive]);
return (
<div className="app">
<button onClick={() => setIsActive((prev) => !prev)} type="button">
{isActive ? "Pause Timer" : "Play Timer"}
</button>
<h3>Seconds: {seconds}</h3>
<h3>Seconds x2: {doubleSeconds}</h3>
</div>
);
};
export { Timer as default };
The problem of stale values will still exist (CodeSandbox Demo (using Reducers)).
The Question(s)
So what is the recommendation for this scenario? Do I take the performance hit and simply add "seconds" to the dependency array? Do I create another useEffect block that depends on "seconds" and call "setDoubleSeconds()" in there? Do I merge "seconds" and "doubleSeconds" into a single state object? Do I use refs?
Also, you might be thinking "Why don't you simply change <h3>Seconds x2: {doubleSeconds}</h3>" to <h3>Seconds x2: {seconds * 2}</h3> and remove the 'doubleSeconds' state?". In my real application doubleSeconds is passed to a Child component and I do not want the Child component to know how seconds is mapped to doubleSeconds as it makes the Child less re-usable.
Thanks!
You can access a value inside an effect callback without adding it as a dep in a few ways.
setState. You can tap the up-to-date value of a state variable through its setter.
setSeconds(seconds => (setDoubleSeconds(seconds * 2), seconds));
Ref. You can pass a ref as a dependency and it'll never change. You need to manually keep it up to date, though.
const secondsRef = useRef(0);
const [seconds, setSeconds] = useReducer((_state, action) => (secondsRef.current = action), 0);
You can then use secondsRef.current to access seconds in a block of code without having it trigger deps changes.
setDoubleSeconds(secondsRef.current * 2);
In my opinion you should never omit a dependency from the deps array. Use a hack like the above to make sure your values are up-to-date if you need the deps not to change.
Always first consider if there's some more elegant way to write your code than hacking a value into a callback. In your example doubleSeconds can be expressed as a derivative of seconds.
const [seconds, setSeconds] = useState(0);
const doubleSeconds = seconds * 2;
Sometimes applications aren't that simple so you may need to use the hacks described above.
Do I take the performance hit and simply add "seconds" to the dependency array?
Do I create another useEffect block that depends on "seconds" and call "setDoubleSeconds()" in there?
Do I merge "seconds" and "doubleSeconds" into a single state object?
Do I use refs?
All of them work correctly, although personally I would rather choose the second approach:
useEffect(() => {
setDoubleSeconds(seconds * 2);
}, [seconds]);
However:
In my real application doubleSeconds is passed to a Child component and I do not want the Child component to know how seconds is mapped to doubleSeconds as it makes the Child less re-usable
That is questionable. Child component might be implemented like the following:
const Child = ({second}) => (
<p>Seconds: {second}s</p>
);
And parent component should look like the following:
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// change seconds
}, []);
return (
<React.Fragment>
<Child seconds={second} />
<Child seconds={second * 2} />
</React.Fragment>
);
This would be a more clear and concise way.

Resources