简体   繁体   中英

How can I avoid scoping problems related to React hooks

The Problem

I have a recurring problem that seems to happen whenever I try and do anything remotely complicated and async using hooks.

The timeline which causes the issue goes something like this:

  1. Define some state in a hook s
  2. Make an async call to get some data d and use it to set state s
  3. Make a subsequent call based on d , and use it to update s

The problem is that at 3.; when I want to update the state s , it at that point is the s as it was defined in the scope at which the function was defined, ie. it has no concept of the latest updated state, only that which it knew about when the function was defined.

My Half-Solutions

I have been able to get around this using a few things, a couple of things which tend to work:

  1. Denormalise state (probably a good idea anyway) such that we avoid as much as possible having to update existing items in state
  2. Run the update after an timeout; use setTimeout to force the update to happen in the next render cycle
  3. Forget using state hooks at all, manage state externally and pass everything in as props

These aren't always (or ever) either good or desirable solutions. I'm guessing I'm missing something fundamental here but I haven't been able to find anything about this online, and I haven't yet had any viable suggestions from my colleagues.

TL:DR

Functions defined within the body of a React functional component will be defined with the scope to which they have access at that time. This isn't always the latest state. How can I get around the problem?


An arbitrary example to demonstrate what it is I'm talking about:

 const getUsers = () => Promise.resolve({ 1: { name: 'Mr 1', favouriteColour: null, }, 2: { name: 'Ms 2', favouriteColour: null, }, }); const getFavouriteColorForUser = (id) => Promise.resolve({ id, color: Math.random() > 0.5 ? 'red' : 'blue' }); const App = () => { const [users, setUsers] = React.useState({}); const handleClick = () => { getUsers() .then(data => { setUsers(data); return Promise.resolve(data); }) .then(data => { return Promise.all(Object.keys(data).map(getFavouriteColorForUser)); }) .then(data => { const updatedUsers = { ...users, ...data.reduce( (p, c) => ({ ...p, [c.id]: { ...users[c.id], favouriteColor: c.color }, }), {} ) }; console.log('users:'); console.dir(users); console.log('color data:'); console.dir(data); console.log('updatedUsers:'); console.dir(updatedUsers); setUsers(updatedUsers); }) } return ( <div> {Object.keys(users).map(k => users[k]).map(user => ( <div>{user.name}'s favourite colour is {user.favouriteColour}</div> ))} <button onClick={handleClick}>Get Users</button> </div> ); } ReactDOM.render(<App />, document.getElementById('app'))
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script> <div id="app"></div>

setState has a variant where you pass it a function. That function is guaranteed to be called with the most recent value from the state. You can use that to calculate your next state:

.then(data => {
  setUsers(previousUsers => {
    const updatedUsers = {
      ...previousUsers, // <--- using previousUsers, not users
      ...data.reduce(
        (p, c) => ({
          ...p,
          [c.id]: {
            ...previousUsers[c.id], // <--- using previousUsers, not users
            favouriteColor: c.color
          },
        }), {})
    };

    return updatedUsers;
  })
})

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM