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

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
}

Related

React Native useEffect with async call results in stale state

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

UI Kitten functional component - Props undefined

I'm developing a react native mobile app using UI Kitten. I'm still fairly new to both react native and UI kitten, so I am using the just the plain Javascript template VS the Typescript template.
I have a functional component screen as shown below. This screen is working just fine. Today I started REDUX implementation.
const RequestScreen = ({ navigation }) => {
// code removed for brevity
}
Within this screen I use the useEffect hook to fetch data from my API
useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
getServices();
});
return () => {
unsubscribe;
};
}, [navigation]);
const getServices = async () => {
setLoading(true);
// helper function to call API
await getAllServices().then((response) => {
if (response !== undefined) {
const services = response.service;
setServices(services);
setLoading(false);
}
});
// update redux state
props.getAllServices(services);
};
// code removed for brevity
const mapStateToProps = (state) => state;
const mapDispatchToProps = (dispatch) => ({
getServices: (services) =>
dispatch({
type: Types.GET_SERVICES,
payload: { services },
}),
});
const connectComponent = connect(mapStateToProps, mapDispatchToProps);
export default connectComponent(RequestScreen);
On this line of code:
props.getAllServices(services);
I keep getting this error:
[Unhandled promise rejection: TypeError: undefined is not an object
(evaluating 'props.getAllServices')] at
node_modules\regenerator-runtime\runtime.js:63:36 in tryCatch at
node_modules\regenerator-runtime\runtime.js:293:29 in invoke
Anytime I try to use "props" in code here. I run into errors. How do I get the props on this screen?
I tried changing the screen, as shown below, but that does not work either!
const RequestScreen = ({ navigation, props }) => {
// code removed
}
I was able to get the props object after changing the screen component as shown below.
const RequestScreen = ({ navigation, ...props }) => {}

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

Queue navigation until screen is mounted and then navigate

I am trying to navigate to a certain screen on my bottom-tab-navigator when a user opens the app by clicking a notification.
Looking into the official docs Navigating without the navigation prop, my setup of my main navigator is as follows:
import {navigationRef, isReadyRef} from './root';
const MainNav = _ => {
if (isLoading) {
return isFirstTime ? (<OnBoarding />) : (<SplashScreen />);
}
return (
<NavigationContainer
ref={navigationRef}
onReady={() => {isReadyRef.current = true}}>
{!token ? <AuthNav /> : <AppNav />}
</NavigationContainer>
);
}
My root.js is as follows:
import * as React from 'react';
export const isReadyRef = React.createRef();
export const navigationRef = React.createRef();
export function navigate(name, params) {
if (isReadyRef.current && navigationRef.current) {
// Perform navigation if the app has mounted
navigationRef.current.navigate(name, params);
} else {
// You can decide what to do if the app hasn't mounted
// You can ignore this, or add these actions to a queue you can call later
console.log('Not mounted yet.')
}
}
And I had added the OneSignal event listener in my root index.js as following:
const App = _ => {
useEffect(() => {
OneSignal.addEventListener('opened', onOpened);
return () => OneSignal.removeEventListener('opened', onOpened);
}, []);
return {
<StoreProvider store={store}>
<MainNav />
</StoreProvider>
}
}
And my onOpened function is as follows:
import {navigate} from '../nav/root';
const onOpened = ({notification}) => {
if(notification.type == 'New Request'){
navigate('Notifications');
}
}
But when I test it as expected Not mounted yet. is printed to console. So I want to
add these actions to a queue you can call later
as stated by the official react navigation docs but I am not sure how to do this. I found react-native-queue but it is no longer being maintained and using a setTimeout just seems like an ugly hack cause the load time varies. So is there a better approach or solution that I can use to navigate only after the loading is done (I am thinking of using redux for this) and my navigators have been mounted (not sure how to do this)?

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;
}