getState() hook proposal
See original GitHub issueuseState()
provided state value currently cannot be used in useEffect(fn, [])
- (componentDidMount-like scenario) with asynchronous functions after state has been updated following the initial [] run.
Upon trying to give Hooks a try for a real world application I was initially confused with accessing state. Here is a little example what I tried to do:
const UserList = () => {
const [users, setUsers] = useState([])
useEffect(() => {
const socket = io('/dashboard')
socket.on('user:connect', (user) => {
setUsers([...users, user])
})
socket.on('user:update', (user) => {
let newUsers = users.map((u) => u.id == user.id ? user : u)
setUsers(newUsers)
})
}, [])
return (
users.map(({id, email}) => (
<tr key={id}>
<td>{id}</td>
<td>{email}</td>
</tr>
))
)
}
Upon running this I instantly realised that inside the socket.on()
handler the users
initially obtained from useState() did not reflect the changes inflicted by setUsers()
ran on socket.on('user:connect')
. Passing [users]
as the second argument of useEffect()
wasn’t an option as that would cause additional socket.on()
binds. I became skeptical about Hooks for this use case and sadly thought this would be where my journey with using hooks instead of the class components would end.
Fortunately I then found a solution to this problem (with someone indirectly having helped me by accident in the reactflux channel) by using an updater function with setState()
which made it all work:
socket.on('user:update', (user) => {
setUsers(users => users.map((u) => u.id == user.id ? user : u))
})
The setState()
problem was solved, but I am now wondering that if I will ever need to access state
outside of an updater function, i.e. to just reply to a WebSocket message with some value from the state I will be unable to do so and this will force me and other users to revert to class components for such cases.
I therefore would like to suggest that a getState()
hook would be an ideal solution to this problem.
Here is another mini example demonstrating the problem in a more concise manner:
const HooksComponent = () => {
const [value, setValue] = useState({ val: 0 });
useEffect(() => {
setTimeout(() => setValue({ val: 10 }), 100)
setTimeout(() => console.log('value: ', value.val), 200)
}, []);
}
//console.log output: 0 instead of 10
And here is one with a proposed solution:
const HooksComponent = () => {
const [state, setState, getState] = useState({ val: 0 });
useEffect(() => {
setTimeout(() => setState({ val: 10 }), 100)
setTimeout(() => {
getState(state => {
console.log('value: ', state.val)
})
}, 200)
}, [])
Issue Analytics
- State:
- Created 5 years ago
- Reactions:10
- Comments:16 (3 by maintainers)
@gaearon, it would be really useful to add the
updaterFn
argument to thedispatch
method returned byuseReducer
, so you could do something like:In that case, state would always point to the freshest state.
This is a known limitation. We might make
useCallback
handle these cases better in the future. We know that in practice it invalidates too often. The current recommended workaround is one of three options:useReducer
orsetState(updaterFn)
to avoid closing over the variables. This doesn’t solve all use cases (especially when you also need to perform side effects) but gets you pretty close.There is also a more hacky workaround using refs that essentially emulates what classes do. I only recommend it as last resort since it may cause some issues in the future, but at least it’s no worse than classes.
We want to provide a better solution but it will take some time.