jamiebuilds / unstated-next

200 bytes to never think about React state management libraries ever again
MIT License
4.18k stars 145 forks source link

Multiple calls in same rendering cycle references old state #48

Open ypresto opened 5 years ago

ypresto commented 5 years ago
function useCounter(initialState = 0) {
  let [count, setCount] = useState(initialState)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

then

counter.increment()
counter.increment()

=> counter.count === 1

For example, this could be caused by immediate error callback of API client.

function useStore(initialState = 0) {
  const [state, setState] = useState(initialState)
  const onInput = (input) => setState({ ...state, input })
  const onRequestStart = () => setState({ ...state, loading: true, sentInput: state.input })
  const onRequestFailed = () => setState({ ...state, loading: false })
  return { state, decrement, increment }
}

const [state, actions] = Store.useContainer()
actions.onRequestStart()
api.call(params, (data, error) => {
  if (error) actions.onRequestFailed() // could reference old state value, "sentInput" value will be lost.
})

Maybe related: #26

Pytal commented 5 years ago

Found a solution using useEffect.

shrugs commented 5 years ago

This is standard react hooks behavior — your count variable is closed-over when creating your decrement behavior. In this case you probably want to use the reducer form of setState or just use useReducer directly.

You'll also want to wrap those callbacks in useCallback to avoid unnecessary re-renders.

function useCounter(initialState = 0) {
  const [count, setCount] = useState(initialState)
  const decrement = useCallback(() => setCount(count => count + 1), [])
  const increment = useCallback(() => setCount(count => count + 1), [])

  return { count, decrement, increment }
}
garretto commented 5 years ago

The answer by @shrugs should be added to the official documentation. I think there are also cases where decrement and increment should just have their own context, without count. That's similar to how useReducer is recommended with context.

https://reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down