Home > Software design >  React Native useEffect with async call results in stale state
React Native useEffect with async call results in stale state

Time:01-09

I have a simplified react native app here that makes a network call and sets a flag when it loads. There is a button onPress handler which calls another method doSomething, both methods which are in a useCallback and the dependency arrays are correct as per the exhaustive-deps plugin in vscode.

When the app loads I can see the isInitialized flag is set to true, however pressing the button afterwards shows the flag is still false in the doSomething method. It seems like the useCallback methods are not being regenerated according to their dependency arrays in this situation.

import React, {useEffect, useState, useCallback} from 'react';
import { Text, View, TouchableOpacity } from 'react-native';


export default function App() {
  const [isInitialized, setIsInitialized] = useState(false);

  useEffect(() => {
    fetch("http://www.google.com").then(() => setIsInitialized(true) );
  }, []);

  const onPress = useCallback(() => {
    doSomething();
  }, [doSomething]);

  const doSomething = useCallback(() => {
    console.log("doSomething", { isInitialized });
  }, [isInitialized]);
  
  return (
    <View style={{flex:1, justifyContent:"center", alignItems:"center"}}>
      {isInitialized &&
        <Text>Initialized</Text>
      }
      <TouchableOpacity onPress={onPress} style={{padding:30, borderWidth:1}}>
        <Text>Press Me</Text>
      </TouchableOpacity>
    </View>
  );
}

Can someone please explain why this happens? Note that the stale state only happens when the flag is set after the network call, and only happens with two hops between methods with useCallback(). If the button onPress is set to doSomething directly, then the flag shows correctly as true.

I am using useCallback in this way all over my code, and I'm afraid of finding stale state in unexpected places due to not understanding something that's going on here.

CodePudding user response:

Similar post here. See also the React docs on useCallback.

When you encapsulate a function in useCallback, you're telling React not to update the function unless one of the dependencies changes. However, a dependency changing in useCallback will not trigger a re-render of the component. Since your useEffect has no dependencies, the component will never be re-rendered with the new values.

You have the following code:

  useEffect(() => {
    fetch("http://www.google.com").then(() => setIsInitialized(true) );
  }, []);

  const onPress = useCallback(() => {
    doSomething();
  }, [doSomething]);

  const doSomething = useCallback(() => {
    console.log("doSomething", { isInitialized });
  }, [isInitialized]);

These three functions could be rewritten to:

  useEffect(() => {
    fetch("http://www.google.com").then(() => setIsInitialized(true) );
  }, []);

  useEffect(() => {
    console.log({ isInitialized }):
  }, [isInitialized]);

  const doSomething = useCallback((isInitialized) => {
    console.log("doSomething", { isInitialized });
  });

This way, doSomething will always have a fresh value passed into it. You would then rewrite your TouchableOpacity like this:

  <TouchableOpacity onPress={() => doSomething(isInitilized)} style={{padding:30, borderWidth:1}}>
    ...

This way, the most current value of isInitialized is ensured, by forcing a re-render of the component in your second useEffect.

I'm not sure about your use case, but useCallback is to be used with care. The point of it is to freeze a function in time and prevent it from being re-initialized. This is only valuable if you have a component that needs to be re-rendered a lot; if you're only doing a single fetch, and that fetch isn't going to happen much, useCallback will cause more problems than it solves for you.

CodePudding user response:

Function doSomething is undefined when you are passing it as a dependency to useCallback, so function doesn't change with isInitialized. Move declaration of doSomething above onPress. Using useCallback everywhere may not be the best idea, but I don't know your use case and I hope you measured performance and gains :)

  •  Tags:  
  • Related