Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hooks useCallback keeps using old values when refreshing list

What is wrong with the useCallback below that I do not get the values below every time the function onRefresh is called ? How can I make it return the expected values using Hooks?

Example when I call onRefresh 2x

values expected:

true
0
20
false

true
0
20
false

values: received

false
0
0
false

false
20
20
false

initialization of the state variables

const [refreshing, setRefreshing] = useState(false)
const [offsetQuestions, setOffsetQuestions] = useState(0)

Function call with useCallback:

const fetchSomeData = async () => {
  await new Promise(resolve => setTimeout(resolve, 3000)) // 3 sec
}

const onRefresh = useCallback( async () => {
  setRefreshing(true)
  setOffsetQuestions(0)
  console.log(refreshing)
  console.log(offsetQuestions)

  await fetchSomeData()

  setOffsetQuestions(20)
  setRefreshing(false)
  console.log(offsetQuestions)
  console.log(refreshing)
}, [refreshing, offsetQuestions])

Where the function is called:

<FlatList
   data={questionsLocal}
   refreshing={refreshing}
   onRefresh={onRefresh}
   ...
/>
like image 612
Roni Castro Avatar asked Feb 20 '20 03:02

Roni Castro


2 Answers

What you are getting is the expected behaviour in hooks. It's all about closures. Each render in react has its own props, state, functions and event handlers and they forever stay the same for that particular render. So whats happening here is that the useCallback is closing over the state variables for that particular render, So the console.log even after a setState will always give you the value of the state for that particular render because of closure.

like image 126
Chitova263 Avatar answered Oct 04 '22 22:10

Chitova263


This situation confuses many developers who start using hooks. You would expect that React will be reactive and will change the value of the state after you called the setX, but if you think about it, you can not expect React to stop the execution flow change the state variable and continue execution.

So, what you are getting is the state variable from the scope of the creation of the useCallback (Read it again and again).

Let's break it down to two parts, the scope, and useCallback, as these are two different things you must understand to be able to use useState and useCallback.

Scope:

Lets say that you have a state called age and a function that write/reads this state (For now, without using useCallback)

const [age, setAge] = useState(42);

const increaseAge = () => {
  setAge(age + 1);   // this "age" is the same "age" from above 
  console.log(age);  // also this one
}

From this you can see that every render a new age variable is defined (The value if given by React), also a new increaseAge function is defined with access to the scope that holds the age variable.

This is how the scope works, this is why you don't get a new value of age just after calling setAge. To be able to get a new value on age React will have to re-render the component, so a new age variable will be defined on a new scope. And this is exactly what happens after calling setAge, React will trigger a new render.

useCallback

Now, let's say that you don't want to define the increaseAge function again and again, you want to keep the same reference (Can help preventing useless renders). This is where you'd want to use useCallback.

When using useCallback, React provides the same function reference each render, unless one of the dependencies changes.

Without giving the useCallback the dependencies, React will not recreate the function and the function will access to the scope that it was created on (First render), when giving the useCallback the correct dependencies, it will be recreated, and now have access to the scope with the latest state variable.

This should explain why you must provide dependencies, and how state variables are accessed (scope).

like image 26
gilamran Avatar answered Oct 04 '22 23:10

gilamran