Can not implement callback inside of useFocusEffect from React-navigation - react-native

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

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

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

Warning when switching to another screen if the user has authorization

When I open the application and if the isAuth variable is true, it means the user has authorization, then I want to go to the provider screen.
My authorization component.
export const LoginScreen: NavigationStackScreenComponent<NavigationParams> = observer(({ navigation }: NavigationParams) => {
useEffect(() => {
AuthState.checkAuthentication();
}, []);
if (AuthState.isAuth) {
navigation.navigate('Provider');
}
return <View style={styles.body}>{LoaderState.loading ? <LoaderComponent /> : <AuthComponent />}</View>;
});
My provider component.
export const ProvidersScreen: NavigationStackScreenComponent<NavigationParams> = observer(({ navigation }: NavigationParams) => {
useEffect(() => {
ProvidersState.setProviders();
}, []);
return (
<View style={styles.body}>
{LoaderState.loading ? (
<LoaderComponent />
) : (
<ItemListComponent itemsList={ProvidersState.providersList} />
)}
</View>
);
});
But I get a warning. I understand that it is associated with the react-navigation library.
How to switch to another screen using a conditional statement?
Functional components are basically the equivalent to the render() function in class components. You are calling navigate before the component gets render and that's a problem.
You can either call the navigate inside your useEffect or put the conditional logic in the parent view.
In the first case, navigate will be called AFTER the component renders
useEffect(() => {
AuthState.checkAuthentication();
if (AuthState.isAuth) {
navigation.navigate('Provider');
}
}, []);
so it will be visible for half a second and then change. If you'd like to avoid this, then go for the second option.
You can put the condition in the parent. Something like
<View>{AuthState.isAuth ? <ProvidersScreen /> : <LoginScreen />}</View>

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
}

React Native Handle Headerback with Redux

I have integrated Redux to my RN Project. I can navigate between screens with buttons, but when I want to go back with the HeaderBackButton it says: "undefined is not a function"
Github-Repo: https://github.com/bayraktarhasan/React-Navigation-Redux-Globalization-Example.git
My Feed Component:
class Feed extends Component {
static navigationOptions = {
title: 'Hello Ahmet',
headerLeft: <HeaderBackButton onPress={this.goBack()} />,
}
constructor(props) {
super(props);
this.goBack = this.goBack.bind(this);
}
goBack = () => {
this.props.navBack();
}
render() {
return (
<View style={styles.container}>
<Text>{I18n.t('feedComponent')}</Text>
<Button
title={I18n.t('back')}
onPress={this.goBack}
/>
</View>
);
}
}
export default connect(null, { navBack, navToProfile })(Feed);
Reducer:
import { NAVIGATE_BACK, NAVIGATE_PROFILE, NAVIGATE_FEED } from '../Actions/types';
const firstAction = AppNavigator.router.getActionForPathAndParams('Main');
const initialNavState = AppNavigator.router.getStateForAction(
firstAction
);
function nav(state = initialNavState, action) {
console.log(action.type);
let nextState;
switch (action.type) {
case NAVIGATE_BACK:
nextState = AppNavigator.router.getStateForAction(
NavigationActions.back(),
state
);
break;
This is just a shot in the dark, but goBack isn't really defined. Maybe try:
const goBack = () => {
this.props.navBack();
}
This is very ironic, because I was having the same problem yesterday. Here's the post How to properly assign a url as a prop to action creator
It might not be very clear from the above link what might need to be done in your case. But I'll try to explain what I've discovered (I'm not an expert in any way). Your code would work on a text input or a date picker or something of that sort. Like this:
<TextInput
label="Model"
placeholder="4020"
value={this.props.model}
onChangeText={value => this.props.entryUpdate({ prop: 'model', value })}
/>
Where the text input has a couple of props already. Here, you assign value to
this.props.<whatever you want the specific object/prop to be called>
But with a button, you have to assign an actual prop name/variable to pass to your action creator. Here you don't have an actual prop. So, it comes back as undefined. So, you have to declare a variable name for that prop and assign it to this.props just before:
this.props.navBack();
So maybe it would be:
const goBack = () => {
const { back } = this.props
this.props.navBack({ back });
}
And in your case, you might need to specify a specific value for the prop { back }. Otherwise, it'll tell you that your second argument is undefined as well.
Here's a cool debugger that helps you see if your props are actually being passed to the action creator:
https://github.com/jhen0409/react-native-debugger
Hope that helps more than it confuses. Good luck!