React Native useEffect with async call results in stale state - react-native

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.

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.

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

Related

how to use useEffect and useCallback, so the screen don't call the function before button clicks

I'm learning useEffect and useCallBack hooks. What I'm trying to do is add a function in the useCallBack, and make sure that the screen will not call this function until I click on a button. Here is my sample code below. Currently, it calls the function inside useEffect before I click on the button. My goal is to click on the button then trigger the useEffect function with useCallBack function. Still new to this, thanks for helping out.
import React, {useState, useEffect, useCallback} from 'react';
import {Button, StyleSheet, View} from 'react-native';
const submitHandler = useCallback(() => {
console.log('the inputs tracking');
}, []);
useEffect(() => {
console.log('Should not be work until submitHandler button is clicked');
}, [submitHandler]);
return (
<View>
<Button title="Click me" onPress={submitHandler}></Button>
</View>
)
I would think you wouldn't need to call submitHandler from useEffect - just delete the useEffect block and perform all the logic in submitHandler.
You're seeing the useEffect run because all useEffects will run on the first render.
If the useEffect is somehow necessary, you would need an additional state to track if the button was clicked.
const [clickCount, setClickCount] = useState(0);
const submitHandler = useCallback(() => {
console.log('the inputs tracking');
}, []);
useEffect(() => {
if (clickCount > 0) {
submitHandler();
}
}, [clickCount, submitHandler]);
return (
<View>
<Button title="Click me" onPress={() => setClickCount((x) => x++)}></Button>
</View>
);
The onPress callback increments the count, which will trigger the useEffect and call submitHandler. Although, I think this is unnecessary and a single useCallback provided to onPress is the right way to go.
Additional comment about your code:
Adding submitHandler in useEffect dependency array does not mean the effect will run when submitHandler is called. The useEffect will run whenever submitHandler changes (it's object reference) and since its dependency array is empty it will never change. This means the useEffect will only fire on the first render.

Some questions about using the useEffect hook

I am new in react-native and the hooks. In my react-native project, I have one screen needs to query data from backend, then, there are some code using the backend returned data should only be run once when the screen mounted. This is what I did (I am using react-query for data fetching from backend):
const MyScreen = ()=> {
// fetch data from backend or cache, just think this code gets data from backend if you don't know react-query
const {status, data, error} = useQuery(['get-my-data'], httpClient.fetchData);
// these code only need to run once when screen mounted, that's why I use useEffect hook.
useEffect(() => {
// check data
console.log(`data: ${JSON.stringify(data)}`);
// a function to process data
const processedData = processeData(data);
return () => {
console.log('Screen did unmount');
};
}, []);
return (<View>
{/* I need to show processed data here, but the processedData is scoped in useEffect hook & I need to have the process data function in useEffect since only need it to be run once */}
</View>)
}
My questions are:
Does react native guarantee the order that the code above useEffect is invoked always first after that run the useEffect code?
As you can see the processedData is returned inside useEffect, how can I pass that return to the layout code to render the processed data?
First question: useEffect is run after the component has fully rendered and does not block the browser's painting. Consider this example:
export default function App() {
console.log("I am code from the app")
React.useEffect(() => {
console.log("I am the effect")
})
React.useLayoutEffect(() => {
console.log("I am the layout effect")
})
return (
<div className="App">
{console.log("I am inside the jsx")}
<h1>Hello World</h1>
</div>
);
}
Will output:
I am code from the app
I am inside the jsx
I am the layout effect
I am the effect
So the useEffect callback will happen as the last thing, after everything else has been done.
Second Question: You can only pass that by using useState and setting the state inside your effect:
const [data, setData] = React.useState()
React.useEffect(() => {
// Your other code
const processedData = processeData(data);
setData(processedData)
}, [setData])

Can not implement callback inside of useFocusEffect from React-navigation

I have React-native app with topTabNavigator with three tabs. And usually componentDidMount and componentWillUnmount lifecycle methods don't work when the user changes the tab. Therefore instead of them I decided to use for the side effects onWillFocus and onDidFocus from React-Navigation. And before 5th version of this great library https://reactnavigation.org/ it was possible to import NavigationEvents component and put it to the view with focused callbacks:
import { NavigationEvents } from 'react-navigation';
class MyTeamScreen {
const store = this.props.store;
const members = store.members;
return (
<View>
<NavigationEvents
onWillFocus={payload => store.getTeamMebers()}
onDidFocus={payload => store.dispose()}
/>
<MymebersList team={members} />
</View>
);
}
export default MyScreen;
But at the moment there is no like this way after the upgrade of react reactnavigation library, because NavigationEvents is deprecated. And only one way to use useFocusEffect. And this is my hook:
function FetchMembers(store) {
useFocusEffect(
React.useCallback(() => {
return () => store.getMembers();
}, [store])
);
return null;
}
Class component:
class MyTeamScreen {
const store = this.props.store;
const members = store.members;
return (
<View>
<FetchMembers store={store} />
<MymebersList team={members} />
</View>
);
}
But I'm getting the error:
And I checked the store was initialized inside of the hook, but it can not call a method from it, because it's undefined.
Can you tell me please what I'm doing wrong? Is it a good way to use react-navigation methods instead of lifecycles componentDidMount and componentWillUnmount? Or maybe you could recommend me please the better way how to implement side effect when the user is changing the tab?
Looking at your code, I'm curious (but not so sure) that you may refer to the incorrect props?
Would you mind trying this?
Because the first parameter of functional component is props. To refer to props.store, you can use object destructuring like this
function FetchMembers({store}) {
useFocusEffect(
React.useCallback(() => {
return () => store.getMembers();
}, [store])
);
return null;
}
or
function FetchMembers(props) {
useFocusEffect(
React.useCallback(() => {
return () => props.store.getMembers();
}, [props.store])
);
return null;
}

Is there a proper way to use useNavigation() hook with useEffect() dependencies?

I'm trying to interact with react-navigation using useNavigation() hook in response to a callback I'm registering in useEffect(). The linter is warning me that useEffect() has a missing dependency. If I add the navigation hook as a dependency, the effect continuously runs. I'm trying to avoid this and wondering if there is a correct way other than ignoring the linter error.
Providing no dependency array results in the same behavior where the effect continuously fires.
This may be an underlying issue with how the useNavigation() hook from react-navigation-hooks package works.
function MyComponent() {
const navigation = useNavigation();
useEffect(() => {
navigation.navigate('Home');
}, []);
}
Results in:
React Hook useEffect has a missing dependency: 'navigation'. Either include it or remove the dependency array.
Just an opinionated guess: It's more a question regarding your "architecture".
For example: Wouldn't it make more sense for the custom useNavigation hook to return a function that can be called by the consumer of the hook instead of an object with all it's functionality?
Here is an example:
const useNavigation = () => {
const [routes, setRoutes] = useState(null);
...
const navigate = (destination: string) => {
console.log("navigated to ", destination);
};
return { navigate, routes };
};
function App() {
const { navigate } = useNavigation();
return (
<div className="App">
<h1>Parent</h1>
<button onClick={() => navigate("Home")}>Navigate me!</button>
</div>
);
}
Working Codesandbox: https://codesandbox.io/s/usenavigation-95kql
If you nevertheless want to keep this "architecture", you could use a useRef hook like so:
const navigation = useRef(useNavigation());
useEffect(() => {
navigation.current.navigate("Home");
}, []);
I believe the error message is clear, you are missing the useEffect dependency:
function MyComponent() {
const navigation = useNavigation();
useEffect(() => {
if (!navigation) return; // <-- this will avoid any undefined or null calls
navigation.navigate('Home');
}, [navigation]); // <-- this dependency
}

Keyboard listener is running more then once

Hey I'm trying to create an event that will fire when the keyboard shows up but the function is firing more then once, I don't know why ..
import React, { Component } from 'react';
import { Keyboard, Alert, View, TextInput } from 'react-native';
export default class App extends Component {
constructor(props: any) {
super(props);
this.kbDidShowListener = Keyboard.addListener('keyboardDidShow', () => Alert.alert('keyboard is up'));
}
componentWillUnmount() {
this.kbDidShowListener.remove();
}
render() {
return (
<View style={{ marginTop: 30 }}>
<TextInput />
</View>
);
}
}
here is an expo for the example (you will see the alert more then once)
https://snack.expo.io/H1DHaIdgM
p.s I'm working on Android.
thanks!
The render function does not run only once. Usually refreshes multiple times too, while calculating the state and props. That could explain the issue.
If you want to be sure, try adding a console too inside the render method, to see if the numbers match.
Actually, another thing I am thinking. Try moving the code to the componentWillMount or componentDidMount
componentDidMount(){
this.kbDidShowListener = Keyboard.addListener('keyboardDidShow', () => Alert.alert('keyboard is up'));
}