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

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

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

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

How to skip nested navigator's initial route?

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
}

How to navigate to a specific screen and disallow going back with react-native-navigation

I am using react-native-navigation and I have a stack of screens.
When I go from screen A to screen B, I don't want to give the user the option to go back to screen A, just forward.
I am trying Navigation.popTo("screen.B.id") but I'm getting this error:
Is there any way to achieve this? Thanks in advance.
React-navigation
You can reset the stack like this:
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'screenB' })],
});
this.props.navigation.dispatch(resetAction);
React-native-navigation
A workaround would be to catch the back listener like so:
import {BackHandler} from 'react-native';
export default class RoomType extends Component {
_didFocusSubscription;
_willBlurSubscription;
constructor(props) {
super(props);
this._didFocusSubscription = props.navigation.addListener('didFocus',payload =>
BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
}
componentDidMount() {
this._willBlurSubscription = this.props.navigation.addListener('willBlur', payload =>
BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
componentWillUnmount() {
this._didFocusSubscription && this._didFocusSubscription.remove();
this._willBlurSubscription && this._willBlurSubscription.remove();
}
onBackButtonPressAndroid = () => {
//code when you press the back button
};
You could try with setting Screen B to your new root.
setStackRoot(componentId, params)
Maybe you have to popToRoot(componentId, mergeOptions?) if necessary.
Source: react-native-navigation docs
In react-native-navigation, there are 2 options you could choose from to achieve what I believe you are looking for.
try adding the below to your topBar option specifically in the child component you are choosing.
backButton: {
visible: false
}
for a small example, the child you don't want a back option for put:
component: {
id: 'screenB',
name: 'screenB',
options: {
title: {
text: 'screenB'
},
topBar: {
// the other options you want
backButton: {
visible: false
}
}
}
}
you can completely reset the root navigation to the new screen.
In my Opinion,
Option 1. is a simple way to flat out just remove the back button from a specific screen to disable the ability to ever go back to the original screen.
Option 2. is nice when you want to remove the previous screen from the entire equation of the app itself.
my personal use-case for option 2:
I made an app that originally opens to a login/register stack. Once logged in/ registered, I save that information to the AsyncStorage and completely reset the root to the home page.
When opening the app for the second time, it checks for user info from the AsyncStorage. If the app finds user information, It sets the root for the home page and rest of the app. If the app doesn't find user information, it sets the root to the login/register stack and the cycle continues.
I hope this helps!
Use createSwitchNavigator from 'react-navigation' for both screens,which will not let back button of second screen to switch to first screen and also header will not be there with back arrow.
In your App.js,
import * as React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { createSwitchNavigator,createAppContainer } from 'react-navigation';
import ScreenOne from './components/ScreenOne ';
import ScreenTwo from './components/ScreenTwo ';
const App=createSwitchNavigator({
ScreenOne :{screen:ScreenOne },
ScreenTwo :{screen:ScreenTwo }
});
export default createAppContainer(App);

React Native Navigation and Redux Persist

I am trying to integrate redux-persist with wix react-native-navigation. However, I am unable to find any examples or documentation stating the boilerplate code needed to integrate the both libraries.
I was wondering if anyone would like to share their solution if they have solved this issue ?
First of all, the basic setup should be the similar with or without react-native-navigation as described in the documentation in store.js:
import { persistStore, persistCombineReducers } from 'redux-persist'
import storage from 'redux-persist/es/storage' // default:
localStorage if web, AsyncStorage if react-native
import reducers from './reducers' // where reducers is an object of
reducers
const config = {
key: 'root',
storage,
}
const reducer = persistCombineReducers(config, reducers)
function configureStore () {
// ...
let store = createStore(reducer)
return store
// We'll skip persistStore for now
// let persistor = persistStore(store)
//return { persistor, store }
}
The persistStore call is commented out as we'll do it below. The persistStore method takes a callback in its third argument. The callback is executed after the state is restored/rehydrated. This is nice because this means we can delay starting the screen(s) until the state is rehydrated.
Let's assume you have the following bootstrap code in App.js:
store = configureStore()
registerScreens(store, Provider)
Navigation.startTabBasedApp({
tabs: [{...},]
})
Now we can add persistStore and wrap your bootstrap code in it like this:
store = configureStore()
persistStore(store, null, () => {
registerScreens(store, Provider)
Navigation.startTabBasedApp({
tabs: [{...},]
})
})
Note:
In v4, you pass config instead of null: persistStore(store, config, callback)
In case you're looking to integrate it with react-native-navigation v2, in App.js, make sure you call persistStore() inside the registerAppLaunchedListener() :
import { persistStore } from 'redux-persist';
...
Navigation.events().registerAppLaunchedListener(() => {
persistStore(store, null, () => {
Navigation.registerComponentWithRedux(...);
...
Navigation.setRoot({...})
...
})
})
Adding to his solution you can also use subscribe() to check if your user is still logged in. That way they don't need to sign in again if they completely close the app (for those users with a login system) and since it is only called once the store is persisted, you can start your app after this is checked.
import {Platform, AsyncStorage, AppState} from "react-native"
import {Navigation} from "react-native-navigation"
import {registerScreens} from "./routes"
import {Provider} from "react-redux"
import configureStore from "./stores/reduxStore"
import {Component} from "react"
const storage = configureStore()
registerScreens(Provider, storage.store)
let startapp = screen => {
Navigation.startSingleScreenApp({
screen: {
screen, // unique ID registered with Navigation.registerScreen
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarColor: "white",
statusBarTextColorScheme: "dark"
}, // override the navigator style for the screen, see "Styling the navigator" below (optional)
navigatorButtons: {} // override the nav buttons for the screen, see "Adding buttons to the navigator" below (optional)
},
drawer: {
left: {
screen: "Drawer", // unique ID registered with Navigation.registerScreen
passProps: {} // simple serializable object that will pass as props to all top screens (optional)
}
},
tabsStyle: {
// optional, add this if you want to style the tab bar beyond the defaults
tabBarButtonColor: "#ffff00", // optional, change the color of the tab icons and text (also unselected). On Android, add this to appStyle
tabBarSelectedButtonColor: "#ff9900", // optional, change the color of the selected tab icon and text (only selected). On Android, add this to appStyle
tabBarBackgroundColor: "#551A8B", // optional, change the background color of the tab bar
initialTabIndex: 1 // optional, the default selected bottom tab. Default: 0. On Android, add this to appStyle
},
appStyle: {
orientation: "portrait"
}
})
}
storage.persistor.subscribe(() => {
storage.store.getState().user.logged
? startapp("mainscreen")
: startapp("loginscreen")
})
We actually dont need redux-persist. We can make our own redux-persist with:
redux + store.subscribe(handlechange)
handleChange function will run when ever something changes in our store.
Also Using aync-await(promise) we are not blocking the main execution thread.
So Inside create store add something like:
store.subscribe(async ()=>{
try {
await AsyncStorage.setItem("store", JSON.stringify(store.getState()));
} catch (error) {
// Error
}
})
Then inside App.js(first component to load). use AsyncStorage.getItem('store'). Then update the store before app starts.
localstorage on the web is a synchronous function which blocks the main thread.
AsynsStorage in react-native doesn't blocks the main thread.