How to skip nested navigator's initial route? - react-native

I have a navigation like so:
Loading: SwitchNavigator {
Auth: Stacknavigator,
Main: StackNavigator,
Onboard: StackNavigator,
}
every one of StackNavigators has an initial route set. Under certain circumstances, I want to go from loading navigator to a specific route on onboard navigator. If I use just navigator.navigate('DesiredRoute'), it squeezes in also onboard's initialRoute, so the navstack looks like [InitialRoute, DesiredRoute] instead of [DesiredRoute]. How to get rid of the InitialRoute?
Code example:
// Loading.js
if (loadingComplete) {
if (!user) {
navigation.navigate('auth')
return
}
if (user && userData) {
navigation.navigate('Main')
return
}
if (onboardingCheckpointCleared) {
// this creates `['InitialRoute', 'DesiredRoute']` instead of `['DesiredRoute']`
navigation.navigate('DesiredRoute')
return
}
navigation.navigate('Onboard')
}

This is expected behavior, if you want only DesiredRoute to appear then you have to set that route in Loading as well like below,
Loading: SwitchNavigator {
Auth: Stacknavigator,
Main: StackNavigator,
Onboard: StackNavigator,
DesiredRoute: ScreenName
}
Writing like this will not create any clash, you are safe to write.

I assume you are navigating to DesiredRoute from outside the Onboard stack navigator
If you're outside the Onboard navigator, doing navigation.navigate('DesiredRoute') will trigger two actions: first, it will navigate to the Onboard stack navigator (so it will also activate the default sub screen of it InitialRoute like you call it) and then it will push the DesiredRoute. If you want to go to Onboard with only DesiredRoute on the stack, you can use the sub actions of the navigation actions like this:
navigation.navigate('Onboard', undefined, StackActions.replace('DesiredRoute'));
The third argument is an action that can be will be executed by the first argument navigator (if the first argument is not a screen but a navigator). Here it will navigate to the Onboard navigator and thus put the InitialRoute in the stack (automatically as it's the initialRoute of Onboard). However, the StackAction.replace('DesiredRoute') will be executed by the Onboard navigator and will replace InitialRoute with DesiredRoute.
See the official doc about the navigate: https://reactnavigation.org/docs/en/navigation-prop.html#navigate-link-to-other-screens

I ended up creating a custom router that strips out the initial route when navigating to my nested stack:
const MyStackNav = createStackNavigator({ ...routes })
const defaultGetStateForAction = MyStackNav.router.getStateForAction
const customGetStateForAction = (action, state) => {
const defaultNavState = defaultGetStateForAction(action, state)
// If we're pushing onto a stack that only has a defaulted initialRoute
if (
!!defaultNavState &&
!defaultNavState.routeName &&
defaultNavState.isTransitioning &&
defaultNavState.index === 1 &&
action.type === NavigationActions.NAVIGATE
) {
const newState = {
...defaultNavState,
index: 0, // Decrement index
routes: defaultNavState.routes.slice(1), // Remove initial route
}
return newState
}
return defaultNavState
}

Related

refetch usequery when go back to previous screen not working in react native

I have 2 page, Page A (current page) and page B (next page). I am using react-native-router-flux as navigation. When go back to page A from page B (Actions.pop()) i want to refetch usequery so i put code like this in page A or component A
const { loading, data, refetch: refetchData } = useQuery(QUERY_GET_STATUS, {
fetchPolicy: 'network-only',
});
useEffect(() => {
if(refresh){
refetchData();
}
}, [refresh])
variable refresh is redux state has value true and false. Before go back to page A refresh state will be update first into true. but i found the issue that refetch query not working. Do you have any solution to resolve it ?
If you wanna call function every time when screen on front then you this hook
import { useFocusEffect } from '#react-navigation/native';
import React{useCallback} from 'react'
useFocusEffect(
useCallback(() => {
//function
}, [])
);
I had a similar problem with a different package. I'm not totally sure if this might work for you but I think with react-native-router-flux, you have access to currentScene. So you could add an effect that is called whenever the route changes
const currentScene = Actions.currentScene;
useEffect(() => {
if(refresh && currentScene === "whatever-scene-you-are-on"){
refetchData();
}
}, [refresh, currentScene])

React navigation "replace" the previous 2 screens instead of only 1

In react navigation, how can I replace the last two screens in a stack?
For example, if my current stack is screen1 -> screen2 -> screen3, when I call .replace('screen4') on screen3,
it becomes screen1 -> screen2 -> screen4
But I want a way so it becomes screen1 -> screen4
You'll need to use reset to do it:
import { CommonActions } from "#react-navigation/native";
// ...
navigation.dispatch(state => {
// Remove the last 2 routes from current list of routes
const routes = state.routes.slice(0, -2);
// Reset the state to the new state with updated list of routes
return CommonActions.reset({
...state,
index: routes.length - 1,
routes
});
});
TL;DR
Scroll down for solution
Long answer
I find #satya164 to be a bit confusing, because the provided piece of code will actually remove the last two screens and simply push you back, unless you go on to push another screen using navigation.push or navigation.navigate.
So the actual solution would be:
import {CommonActions, StackActions} from '#react-navigation/native';
// this removes last two screens
navigation.dispatch(state => {
const routes = state.routes.slice(0, -2);
return CommonActions.reset({
...state,
index: routes.length - 1,
routes,
});
// can also be replaced by:
// return StackActions.popToTop();
});
// and pushes a new one directly
navigation.navigate('YourNewScreen', { /* optional params */ });
But even then, at least on iOS the animation will go backwards, as if a screen is being popped (i.e: user going backwards), instead of being pushed, which isn't very intuitive, and looks a bit weird.
I found that if you want to navigate to a new stacked screen, but want to discard the rest of the stack. The best way to do that, is to navigate to the needed screen, and to discard the rest of the screens keeping the first and last one (the one being presented) after the navigation has happened. This way, yo get the desired behavior and there are no weird jumps or wrong navigations animations.
Another plus is that this way you don't care how many screens you got in between, you just take the first and last. You can then put it in a neat little generic hook:
Solution
import {CommonActions} from '#react-navigation/native';
// call this hook in the last screen of the stack (so `screen4`)
export const useResetNavigationStackEffect = (props, condition) => {
const {navigation} = props;
useEffect(() => {
if (condition) { // only reset if `true`
navigation.dispatch(state => {
const topScreen = state.routes[0];
const thisScreen = state.routes[state.routes.length - 1];
const routes = [topScreen, thisScreen];
return CommonActions.reset({
...state,
index: routes.length - 1,
routes,
});
});
}
}, [condition, navigation]);
};

Going back and forward between different stacks via "Back" key

Here is my code:
const Stack1 = createStackNavigator(
{
Aone: AoneScreen,
Atwo: AtwoScreen,
}
const Stack2 = createStackNavigator(
{
Bone:BoneScreen,
Btwo: BtwoScreen,
}
const Stack3 = createStackNavigator(
{
Cone:ConeScreen,
Ctwo: CtwoScreen,
}
const TabNavigator = createBottomTabNavigator(
Stack1,
Stack2,
Stack3
)
When I'm in a stack like "AoneScreen" and I move into another stack, say "CtwoScreen", and then press "back" button, instead of moving back to the first stack AoneScreen, it moves to the top of the second stack (ConeScreen) As it should! But that's not what I desire. what I want is go back to the original stack as the back button is pressed (in this case "AoneScreen" ) and I was wondering if that's possible.
You can implement custom behaviour for back button on screen "CtwoScreen" with BackHandler from React Native, it only depends on how dynamic this needs to be.
From documentation:
import { BackHandler } from "react-native";
componentDidMount() {
this.backHandler = BackHandler.addEventListener('hardwareBackPress',this.handleBackPress);
}
componentWillUnmount() {
this.backHandler.remove()
}
handleBackPress = () => {
// add some kind of custom check if you want this behaviour only sometimes, nav params are a good way
if (this.props.navigation.state.params.isFromAOneScreen) {
this.props.navigation.setParams({ isFromAOneScreen: false });
this.props.navigation.popToTop(); // to remove COneScreen from stack, from transition
this.props.navigation.navigate("AOneScreen");
return true;
}
return false;
}

Deep linking into an already open screen with different params. Is it possible?

I'm working on an app which has a category screen where the relevant posts of the category are displayed. I'm using react-navigation to navigate between the screens and handle the deep linking. The category screen can be accessed in-app or via deep link. To access the category screen via deep link I'm using something like myapp://category/:id.
If the app is already opened and is focused on the category screen the deep link does nothing.
I've currently "fixed" it using the componentDidUpdate life cycle method to compare the ID stored in the state and the ID in navigation.getParam.
componentDidUpdate = () => {
const { navigation } = this.props;
const { categoryID } = this.state;
const paramID = navigation.getParam('id', null);
if (paramID !== categoryID) {
this.setState({ categoryID: paramID }, () => {
// fetch data
});
}
}
However, this seems like a dirty fix to me. Is there a better way of doing this? Maybe opening all deep links using push instead of navigate via react-navigation?
For anyone who got here wondering how this can be done, like myself, here's the solution:
You can use the getId Stack.Screen property. You just need to return the unique ID from the getId property. Here's an example:
Let's say we have a UserScreen component, which is part of our Stack navigator. The UserScreen has one route param: userId. We can use the getId property like this:
import { createStackNavigator } from '#react-navigation/stack';
const Stack = createStackNavigator();
const AppStack = () => {
const getUserRouteId = ({ params }) => {
return params && params.userId;
};
return (
<Stack.Navigator>
{/* Other routes */}
<Stack.Screen name="User" component={UserScreen} getId={getUserRouteId} />
</Stack.Navigator>;
);
};
Now when you try to normally navigate to this route, using the navigate function, if the param userId in the navigation action is different than on the UserScreen which you're currently on, it will navigate you to a new user screen. React Navigation deep linking uses the navigate function internally.

How to navigate in mobx store using react navigation?

I can use this.props.navigation from screen component to navigate. How should I do the similar in mobx store file? Or should I perform navigation in store?
I read the Navigating without the navigation prop article, but it seems only works for screen components, right?
Someone says use global variable to store a this.props.navigation reference and use it anywhere, but I don't like the idea...
Yes either:
forward the navigation class to the store when calling the method:
// add nivagation parameter to store fucntion:
this.props.store.handleSomething(data, this.props.navigation);
Or you can singleton the navigator (warning only works for one ofc):
return <Navigator ref={(nav) => this.props.store.navigator = nav} />;
after this is rendered it will set the navigator property in the store.
But I would suggest to store all your routing state also in a routing store like this: https://github.com/alisd23/mobx-react-router.
This way you always have easy access to the navigation state and you can be sure everything properly re-renders. (when in render function in components also depends on navigation changes!)
You can keep all your states including navigation state in mobx store.
For example:
// sourced and modified from https://github.com/react-community/react-navigation/issues/34#issuecomment-281651328
class NavigationStore {
#observable headerTitle = "Index"
#observable.ref navigationState = {
index: 0,
routes: [
{ key: "Index", routeName: "Index" },
],
}
// NOTE: the second param, is to avoid stacking and reset the nav state
#action dispatch = (action, stackNavState = true) => {
const previousNavState = stackNavState ? this.navigationState : null;
return this.navigationState = AppNavigator
.router
.getStateForAction(action, previousNavState);
}
}
// NOTE: the top level component must be a reactive component
#observer
class App extends React.Component {
constructor(props, context) {
super(props, context)
// initialize the navigation store
this.store = new NavigationStore()
}
render() {
// patch over the navigation property with the new dispatch and mobx observed state
return (
<AppNavigator navigation={addNavigationHelpers({
dispatch: this.store.dispatch,
state: this.store.navigationState,
addListener: () => { /* left blank */ }
})}/>
)
}
};
Then you can directly call the dispatch action of the store to navigate to a new screen.
Send this one this.props.navigation as a parameter to the store. Then use as you use on the component side.
LoginStore.login(this.props.navigation)
in the LoginStore
#action login = (navigation) => { navigation.navigate('Page');}