Description
useState()
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)
}, [])