Do you want to request a feature or report a bug?
What is the current behavior?
Code from Introducing Hooks:
import { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
```javascript
// each time "count" changed, this arrow function will be created again.
// so that it can access the latest "count"
onClick={() => setCount(count + 1)}
I don't think it is good to create a fixed function many times, so I try to modify the code:
```javascript
const [count, setCount] = useState(0);
const handleClick = useCallback(() => setCount(count + 1), []);
But obviously the callback in useCallback
couldn't get the latest count
because I pass in an empty inputs array to avoid this callback been generated again and again.
So, in fact, the inputs array decide two things:
In most situation, the two things are one thing, but here they conflict.
So I think maybe it's good to add a get
function to useState
like this:
import { useState, useCallback } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount, getCount] = useState(0);
const handleClick = useCallback(() => setCount(getCount() + 1), []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>
Click me
</button>
</div>
);
}
Maybe it's confusing because getCount
can totally replace count
, but it brings the possible to avoid creating callbacks again and again.
https://github.com/facebook/react/issues/14543#issuecomment-452237355 exactly resolves the case above. But thereāre many other scenarios can't use updater
to resolve. Here are some more code snippets:
useEffect(() => {
// or setInterval
const id = setTimeout(() => {
// access states
}, period);
return () => clearTimeout(id);
}, inputs);
useEffect(() => {
// create a WebSocket client named "ws"
ws.onopen = () => {
// access states
};
ws.onmessage = () => {
// access states
};
return () => ws.close();
}, inputs);
useEffect(() => {
create_a_promise().then(() => {
// access states
});
}, inputs);
useEffect(() => {
function handleThatEvent() {
// access states
}
instance.addEventListener('eventName', handleThatEvent);
return instance.removeEventListener('eventName', handleThatEvent);
}, inputs);
We had to use some workaround patterns to resolve those cases, like
https://github.com/facebook/react/issues/14543#issuecomment-452676760
https://github.com/facebook/react/issues/14543#issuecomment-453058025
https://github.com/facebook/react/issues/14543#issuecomment-453079958
Or a funny way:
const [state, setState] = useState();
useEffect(() => {
// or setInterval
const id = setTimeout(() => {
// access states
setState((prevState) => {
// Now I can do anything with state...š¤®
...
return prevState;
});
}, period);
return () => clearTimeout(id);
}, inputs);
So let's discuss and wait...
https://github.com/facebook/react/issues/14543#issuecomment-452713416
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:
What is the expected behavior?
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
@liyuanqiu you can use updater function in setCount
const handleClick = useCallback(() => setCount(prevCount => prevCount + 1), []);
@liyuanqiu you can use updater function in setCount
const handleClick = useCallback(() => setCount(prevCount => prevCount + 1), []);
@Saranchenkov Thank you very much, it's all my fault haven't read the document carefully.
But I have another question. As I said before:
So, in fact, the inputs array decide two things:
- when to recreate the callback
- which state can be accessed in the callback
Sometimes I want to do some side effect in hooks like useEffect
, for example:
const [count, setCount] = useState(0);
useEffect(() => {
// send count to server every 5 seconds
const id = setInterval(() => {
xhr(count);
}, 5000);
return () => clearInterval(id);
}, []);
If I pass [count]
to useEffect
, the interval will be cleared and recreated.
If I pass []
to useEffect
, I can not get the latest count.
In this situation, maybe a get function is needed?
You could probably do something like this
const [count, setCount] = useState(0);
const countRef = useRef(count)
useEffect(() => {
countRef.current = count
}, [count])
useEffect(() => {
// send count to server every 5 seconds
const id = setInterval(() => {
xhr(countRef.current);
}, 5000);
return () => clearInterval(id);
}, []);
You could probably do something like this
const [count, setCount] = useState(0); const countRef = useRef(count) useEffect(() => { countRef.current = count }, [count]) useEffect(() => { // send count to server every 5 seconds const id = setInterval(() => { xhr(countRef.current); }, 5000); return () => clearInterval(id); }, []);
Thank you @escaton , useRef
really can solve this problem.
And the official document thinks this is convoluted
but bearable
:
This is a rather convoluted pattern but it shows that you can do this escape hatch optimization if you need it. Itās more bearable if you extract it to a custom Hook
But I think it is more like a workaround, couldn't be a paradigm.
One component may have many states. Using three kinds of hooks and five lines of code(or using a custom hook to replace useState
) to define an internal state will be a disaster.
I really like the suggestion for having pair of getter and setter returned from useState
.. which would make it easier to keep things fresh.. if it doesn't end up in the official implementation, I think it can be implemented in user land using custom hook like:
const useGetterState = (initialState) => {
const [state, setState] = useState(initialState);
const stateRef = useRef(state);
useEffect(() => {
stateRef.current = state
}, [state]);
return [() => stateRef.current, setState];
}
?
@liyuanqiu
I see such options:
useStateWithRef()
hook which could decorate the original useState
and mirror value to ref.current
const [count, setCount] = useState(0);
const timerAdjustment = useRef(0)
useEffect(() => {
let id;
let absoluteTimeout;
function tick(firstTime) {
// call xhr only on subsequent calls
!firstTime && xhr(count)
// schedule timer considering previous call
const adjustment = timerAdjustment.current
const timeout = adjustment > 0 ? adjustment : 5000
// remember absolute time to calc adjusted timeout later
absoluteTimeout = Date.now() + timeout
// reset timer adjustment
timerAdjustment.current = 0
id = setTimeout(tick, timeout)
}
tick(true)
return () => {
clearTimeout(id)
// set timer adjustment
timerAdjustment.current = absoluteTimeout - Date.now()
}
}, [count])
@ignatiusreza there is the problem with your solution:
[getCount, setCount] = useGetterState(0)
return (
<button onClick={() => setCount(c => c+1)}>
{getCount()} - increment
</button>
)
on the first render in would be "0 - increment", but after click it would be still "0 - increment" and only on second click it will update. That's because you mutate the reference in useEffect
which is fired _after_ component renders.
And while it is fixable:
function useStateWithGetter(initial) {
const [state, setState] = useState(initial)
const ref = useRef(state)
const updater = value => {
if (typeof value === 'function') {
setState(prev => {
const result = value(prev);
ref.current = result
return result
})
} else {
ref.current = value
setState(value)
}
}
const getter = () => ref.current
return [state, updater, getter]
}
there are still issues, because now we referencing _last scheduled_ state, not the current.
upd:
Hmm, what if...
function useStateWithRef(initial) {
const [state, setState] = useState(initial)
const ref = useRef()
ref.current = state
return [state, setState, ref]
}
Just to set expectations, weāve considered all these options a few months ago and decided against them at the time. Iāll keep this open so we can later provide a better response. I don't remember off the top of my mind what the problems were.
I think this is an elegant solution: https://codesandbox.io/s/m1y7vl0vp
function App() {
const [count, setCount] = useState(0);
// ***** Initialize countRef.current with count
const countRef = useRef(count);
const handleClick = useCallback(() => setCount(add1), []);
useEffect(() => {
// ***** countRef.current is xhr function argument
const intervalId = setInterval(() => xhr(countRef.current), 5000);
return () => clearInterval(intervalId);
}, []);
// ***** Set countRef.current to current count
countRef.current = count;
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
@liyuanqiu
I see such options:
- use ref for holding the whole callback rather then single state
- create custom
useStateWithRef()
hook which could decorate the originaluseState
and mirror value toref.current
- reimplement timer logic so it would be able to resume after state changing
@escaton Maybe only the second option is effective.
- use ref for holding the whole callback rather then single state
Do you mean https://codesandbox.io/s/jj40lz07l3 ?
/* code snippets */
const [count, setCount] = useState(0);
const repeat = useRef(() => xhr(count));
useEffect(() => {
const intervalId = setInterval(repeat.current, 1000);
return () => clearInterval(intervalId);
}, []);
Not working too.
- reimplement timer logic so it would be able to resume after state changing
It's not the timer's problem. When you use event dispatcher, WebSocket, ajax, promise, same problem. I think no one dares to reimplement them just for adopting React Hooks API.
@btraljic yeah, i suggested the same thing
function useStateWithRef(initial) { const [state, setState] = useState(initial) const ref = useRef() ref.current = state return [state, setState, ref] }
it should work, but it brings side effect countRef.current = count;
in the component body rather then in useEffect
and that confuses.
@liyuanqiu https://codesandbox.io/s/72jlzz1o86
+useEffect(
+ () => {
+ repeat.current = () => xhr(count);
+ },
+ [count]
+ );
-const intervalId = setInterval(repeat.current, 1000);
+const intervalId = setInterval(() => repeat.current(), 1000);
It's not the timer's problem. When you use event dispatcher, WebSocket, ajax, promise, same problem. I think no one dares to reimplement them just for adopting React Hooks API.
By the reimplementation i mean the proper restart of effect, not the setInterval
itself. There is nothing wrong with it, you just want different behaviour.
Could you provide different example with WebSocket or promise?
@btraljic Thank you.
I think this line of code been written in the function body is not encouraged by Hooks API:
countRef.current = count;
@see https://reactjs.org/docs/hooks-reference.html#useeffect
Mutations, subscriptions, timers, logging, and other side effects are not allowed inside the main body of a function component (referred to as Reactās render phase). Doing so will lead to confusing bugs and inconsistencies in the UI.
Any state change will lead this line been executed. Although in this example, there's no wrong.
If doing so, I think maybe it's better to put it in useLayoutEffect
:
useLayoutEffect(() => {
countRef.current = count;
});
And think deeper, here we rely on the layout update to update countRef
. Actually, it does not rely on layout update, it relies on the change of state: count
.
So finally, it turns back to:
https://github.com/facebook/react/issues/14543#issuecomment-452676760
Is it ok now :)
useEffect(() => countRef.current = count);
@btraljic not really :)
now you have actual value in reference and can use it in next effects, but if once you decide to use it in markup, you would see lag between state
and ref
. Look at example in my answer to @ignatiusreza
@escaton Ok, but we are living in an asynchronous world. Aren't we? :)
Speaking about accessing state in setInterval
ā i came up with another idea.
It can be treated as two separate side effects: one is timer tick, another is xhr
.
So it could be
const [count, setCount] = useState(0)
const [tick, setTick] = useState(0)
useEffect(() => {
const timerId = setInterval(() => {
setTick(t => t+1)
}), 5000)
return () => clearInterval(timerId)
}, [])
useEffect(() => {
xhr(count)
}, [tick])
Pros: easy to understand what happens, no refs
Cons: wasteful rerenders on tick
updates every 5 seconds
I usually use this way with reducer
const [count, setCount] = useState(0)
const [commitIndex, commit] = React.useReducer(state => state +1, 0)
useEffect(() => {
const timerId = setInterval(commit, 5000)
return () => clearInterval(timerId)
}, [])
useEffect(() => {
xhr(count)
}, [commitIndex])
If we can use reft to hold state, why not just use an global object?
const obj = {}
function Comp() {
const [count, setCount] = useState(0)
useEffect(() => {
xhr(obj.count)
})
obj.count = count
}
So obj
may hold multi state for one component.
Doing so, you can't use more than one Comp
on the page, but it could be fixed
function CompFabric () {
const obj = {}
return function Comp() {
const [count, setCount] = useState(0)
useEffect(() => {
xhr(obj.count)
})
obj.count = count
}
}
Although it's almost same as useRef
i strongly discourage you to use it. It is both non idiomatic and confuses other contributors.
Everybody here tells many solutions, but really like a sentence: "Life, Uh, Finds a Way."
Just a joke, no offense :)
Look back, my requirement is so easy, just want to repeatedly send a state to server. But with React Hooks API, it became strange and complex.
Let's see how to program in old class style:
https://codesandbox.io/s/40p9qqr009
class App extends React.Component {
state = {
count: 0,
};
handleClick = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
xhr = () => {
const { count } = this.state;
console.log(`Send ${count} to server.`);
// TODO send count to my server by XMLHttpRequest
};
componentDidMount() {
this.intervalId = setInterval(this.xhr, 1000);
}
componentWillUnmount() {
clearInterval(this.intervalId);
}
render() {
const { count } = this.state;
return (
<div>
<p>You clicked {count} times</p>
<button onClick={this.handleClick}>Click me</button>
</div>
);
}
}
Naturally and reasonable, right? No magic, no tricks.
I hope Hooks API will finally be so.
I complain a lot, don't diss me š XD
Sure, it looks familiar.
The only difference with hooks here is that component's state
is explicitly bounded to instance and so could be accessed with this
anywhere.
To achieve the same in functional component it needs to break the rule and to bound state to ref right in the component body without useEffect
wrapping.
Let's wait for @gaearon asnwer.
I guess this is it https://overreacted.io/making-setinterval-declarative-with-react-hooks/
:)
I guess this is it https://overreacted.io/making-setinterval-declarative-with-react-hooks/
:)
In this post, a pattern is introduced to capsulate those APIs who has āimpedance mismatchā with the React programming model.
So we have to write helper functions(custom hooks) to help us using these APIs. That's still annoying.
Although Dan's useInterval
brings many great features like dynamic delay
and pause and resume
to setInterval
, but that's not the first motivation to write useInterval
. Those great features are just derivatives.
We may encounter many APIs that has āimpedance mismatchā with the React programming model. Capsulating them one by one just like @types/xxx
in Typescript
is hard. Maybe I should create an organization named DefinitelyHooked
š.
This is an early time for Hooks, and there are definitely still patterns we need to work out and compare. Donāt rush to adopt Hooks if youāre used to following well-known ābest practicesā. Thereās still a lot to try and discover.
Ok so it seems like the solution to this is this:??
function useStateWithRef(initial) {
const ref = useRef();
const [state, setState] = useState(initial);
ref.current = state;
useEffect( () =>{
ref.current = state;
});
return [state, setState, ref];
}
I def see the value in having a getter on hooks.
On second thought.. why not just do this??
function useStateWithGetter(initial) {
const ref = useRef();
const [state, setState] = useState(initial);
ref.current = state;
const set = (value) => {
ref.current = value;
setState(value);
};
const get = () => {
return ref.current;
};
return [state, set, get];
}
mark
@gaearon You mentioned this was discussed previously and left it open. Can we start this conversation back up? Or at the very least, verify that the solution above is an ok practice.
https://overreacted.io/how-are-function-components-different-from-classes/
This article may help to understand the behavior of Hooks API(actually is Functional Component).
It's highly recommended to spend half an hour to read it.
Just read that article... it basically reassures the solution above would work for adding a getter.
A Complete Guide to useEffect
touches this subject quite a lot too. Really recommend reading it through, it was an eye opener for me.
Most helpful comment
@liyuanqiu you can use updater function in setCount
const handleClick = useCallback(() => setCount(prevCount => prevCount + 1), []);
useState
API reference