extra functionality of the state hook:
potential performance problem: the intial state is recreated on every render - and only used on the first render
const [foo, setFoo] = useState(
createObjectWithManyEntries('foo')
);
solution: only do data initialization on the first call - by passing in a function which will only be used on the first render
const [foo, setFoo] = useState(() =>
createObjectWithManyEntries('foo')
);
basic version for updating state:
setCount(count + 1);
alternative version that may avoid outdated state:
setCount((c) => c + 1);
(see next section)
Sometimes, we may not have direct access to the most recent state entries when computing a new state
particular scenario: asynchronous events in function components (e.g. network requests)
scenarios:
if an event handler function triggers a state setter, the new state is only available after the event handler has finished executing
when an asynchronous action is triggered in a function component, it may keep referencing old state during its execution
example 1:
const [count, setCount] = useState(0);
function incrementTwice() {
setCount(count + 1);
setCount(count + 1);
}
this code will set count
to 1 twice in a row
example 2:
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
function incrementWithDelay() {
setTimeout(() => setCount(count + 1), 3000);
}
scenario for example 2:
count
starts at 0incrementWithDelay
is calledincrement
is calledincrement
is called againincrementWithDelay
updates the valuevalues of count
: 0 → 1 → 2 → 1
explanation of example 2:
difference of function components and class components when props / state change:
this.props
and this.state
will be replaced with new objectsnote: in function components, old data may still live on inside older closures
example: buggy code that will keep referencing outdated state in a closure
function Counter() {
const [count, setCount] = useState(0);
function startCounting() {
setInterval(() => {
// This innermost function will only be created once.
// The variable "count" will always refer to the
// state from the initial rendering (0)
setCount(count + 1);
}, 1000);
}
return (
<div>
<h1>{count}</h1>
<button onClick={() => startCounting()}>start</button>
</div>
);
}
ESLint rule that can help identifying obsolete data in an effect hook:
react-hooks/exhaustive-deps
(in VS Code, install the ESLint plugin)
possible solutions depending on the scenario (see hints in the linter messages):
possible fix: use a "state transformer function"
change this:
setCount(count + 1);
to this:
setCount((c) => c + 1);
The inner function will always receive the most recent value
possible fix: store data in a ref as well (will be explained in more detail later)
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
function startCounting() {
setInterval(() => {
countRef.current++;
setCount(countRef.current);
}, 1000);
}
// ...
}