React: getState() hook proposal

Created on 4 Nov 2018  路  14Comments  路  Source: facebook/react

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

Most helpful comment

You can useReducer or setState(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.

@gaearon, it would be really useful to add the updaterFn argument to the dispatch method returned by useReducer, so you could do something like:

const [, dispatch] = useReducer(reducer, initialState)
dispatch(state => state.isLoading || doSomething())

In that case, state would always point to the freshest state.

All 14 comments

I don't think this is necessary, you need to clean up your socket.on calls by the way.

With this you can pass the state as the second argument of useEffect and each time the your state changes you socket listeners will be clean up and resubscribe.

Btw I think this is wanted that you cannot access new state in useEffect. It's avoiding misuse of it.

With this you can pass the state as the second argument of useEffect and each time the your state changes you socket listeners will be clean up and resubscribe.

Unsubscribing and subscribing sockets on every message would be a very idea, performance wise the least

This is intended, though I think there is a way around this by using useReducer instead of useState (see more here).

With this you can pass the state as the second argument of useEffect and each time the your state changes you socket listeners will be clean up and resubscribe.

Unsubscribing and subscribing sockets on every message would be a very idea, performance wise the least

Probably better to split it up in two effects! (connect as a separate effect)

Yup the idea of @thchia to use useReducer is brilliant, you could dispatch and action with the new user, which internally would be merge with the current user list.

Like this:

const usersReducer = (users = [], actions = {}) => {
  switch (action.type) {
    case 'ADD_USER': return [...users, action.payload.user]
    case 'UPDATE_USER': {
      const { user } = action.payload
      return users.map((u) => u.id == user.id ? user : u)
    }
    default: return users
  }
}

const UserList = () => {
    const [users, dispatch] = useReducer([])
    useEffect(() => {
        const socket = io('/dashboard')
        socket.on('user:connect', (user) => {
            dispatch({ type: 'ADD_USER', payload: { user } })
        })
        socket.on('user:update', (user) => {
            dispatch({ type: 'UPDATE_USER', payload: { user } })
        }) 
    }, [])

    return (
        users.map(({id, email}) => (
            <tr key={id}>
                <td>{id}</td>
                <td>{email}</td>
            </tr>
        ))
    )
}

EDIT: An example with custom event (instead of sockets connections)
https://codesandbox.io/s/8n6kvqqj3j

I have considered useReducer before, but I do not wan't to use 'redux' or any other boilerplate in this instance. I want simplicity and in this case it turns out that it can be better achieved with class components.

reducer does not mean "Redux", it's just a "reducer" pretty much like "Array.prototype.reduce"
You can try to use "useRef" which could possible let you access the current value at any time.

@ChibiBlasphem this again means boilerplate or jumping through hoops, I expect React Hooks to make my life simpler not more complicated.

Hum I don't think hooks mean to make life "simplier", your code is better designed with less possibility to break

Nothing stops you from doing this
When calling the "setUsers" returned by the custom hook, the ref is updated too.

function useUserList(initialUsers = []) {
  const [users, setUsers] = useState(initialUsers)
  const usersRef = useRef(users)

  return {
    users: usersRef,
    setUsers: function(mapper) {
      usersRef.current = mapper(usersRef.current)
      setUsers(mapper)
    },
  }
}

const UserList = () => {
  const { users, setUsers } = useUserList()
  // ...
}

Refs are made for this:
https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables

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:

  1. Re-subscribe if it's cheap enough.
  2. If re-subscribing is expensive, you could wrap the API you're using to make it cheap.
  3. You can useReducer or setState(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.

I filed https://github.com/facebook/react/issues/14099 to track the root problem.

You can useReducer or setState(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.

@gaearon, it would be really useful to add the updaterFn argument to the dispatch method returned by useReducer, so you could do something like:

const [, dispatch] = useReducer(reducer, initialState)
dispatch(state => state.isLoading || doSomething())

In that case, state would always point to the freshest state.

How about a 3rd parameter on a useEffect hook, where the 3rd parameter lists variables that should pass in updated values, but not trigger execution of the useEffect. Kind of a hybrid of a useCallback and useEffect.

So rewriting the original proposal's example:

const HooksComponent = () => {
  const [state, setState] = useState({ val: 0 });

  useEffect(() => {
    setTimeout(() => setState({ val: 10 }), 100)
    setTimeout(() => {
      console.log('value: ', state.val)
    }, 200)
  }, [], [state])
}

@furnaceX it can't work. You can't update the variable state inside the callback without reexecuting the function... That's how closure are working. It keeps the value at the moment the function were declared.

Was this page helpful?
0 / 5 - 0 ratings