1

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.

Doug
  • 23
  • 2

2 Answers2

0

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.

Abe
  • 4,500
  • 2
  • 11
  • 25
  • Thanks for the response, and I read the other post and the documentation but I still don't get why my code doesn't work as written. When setIsInitiatlized(true) is called after the fetch, wouldn't that trigger a re-render, at which time the two methods encapsulated by useCallback would get re-generated based on their dependencies having been changed? – Doug Jan 08 '22 at 21:38
  • `useEffect` triggers re-renders, `useCallback` does not. – Abe Jan 08 '22 at 22:00
  • I don't believe useEffect triggers re-renders, useEffect would be triggered after a render if any of its dependencies have been changed. However, setting a state variable should trigger a re-render. – Doug Jan 08 '22 at 22:52
-1

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 :)

pafry
  • 239
  • 2
  • 3
  • This is happening because of the rules of closures in javascript, not because the function is undefined at time of render. If the function was undefined, you would see different behavior than the OP is describing. – Abe Jan 08 '22 at 19:20
  • Apparently, the order of the methods does indeed matter - I switched the placement of onPress and doSomething methods and it works! I definitely will be scaling back my usage of useCallback from now on. – Doug Jan 08 '22 at 22:12