How to goBack globally between stacks in React Navigation? - react-native

I am using react navigation ("#react-navigation/native": "^5.1.3") and I have the following setup:
BottomNavigation
-stack1
-stack2
It looks like goBack() is local to the stack. What that means is that if I navigate from a page in stack1 to a page in stack2, I am unable to go the the page I came up from.
Solutions (or rather hacks) that didn't work for me:
pass the source screen as param and then navigate. That isn't a real back button and does not play well with android back button.
Put all pages in bottom navigation. Bottom navigation does not have animations it seems, so I can not achieve the right transitions
Put all pages in stack navigation. In this case I lose the fixed bottom navigation. I can add it to each page, but when transitioning it will go away with the old screen and come again with the new one, which is undesirable.
So I am wondering if I am missing something big here, like a globalBack() that I overlooked?!
And also, I am looking for a solution to this problem which remains.

Naturally if you have bottoms tabs with each tab having its own stack navigator, calling navigation.goBack() will go back from one screen inside stack navigator to previous screen inside that same stack navigator. That's how navigation works in pretty much every app. Pressing back button or swiping back does not change tab for you, tabs are more like separate small apps by itself. If you want to specifically jump from one tab to another instead of going back in stack, use navigation.dispatch(TabActions.jumpTo('Profile')). If pressing something inside tab#1 makes you go to to tab#2 then this screen most likely also belongs to tab#1
also, take a look at backBehavior prop of Tab.Navigator, it might be doing what you want depending on what exactly it is you want https://reactnavigation.org/docs/bottom-tab-navigator/#backbehavior

I'm using bottom tab navigator with 2 stacks as well. I faced similar issue and agree with #Max explanation. Due to my Notification screen is in Stack 1, I have to goBack to Notification after navigating away to Detail screen. After searching for the fix, I'm using this workaround (for v6).
Tab 1 - Stack 1 (Home > Notification screen)
Tab 2 - Stack 2 (Reward Home > Reward Detail screen)
I passed a param when navigating from Notification to RewardDetail. Then I override the headerLeft and use BackHandler to handle Android back function.
Notification.js
navigation.navigate('RewardStack', {
screen: 'RewardDetail',
initial: false,
params:{notification: notification_data_source}
})
RewardDetail.js
const payload = route.params.notification
//1. override headerLeft button
useLayoutEffect(() => {
if(payload)
navigation.setOptions({
headerLeft: () => (
<Button
TouchableComponent={TouchableOpacity}
buttonStyle={{paddingTop:4, paddingLeft:0}}
type='clear'
icon={<Icon name={'chevron-left'} size={30} style={{color:'#FFF'}} />}
onPress={()=>{
navigation.goBack()
navigation.navigate('Notification') //can use this only
}}
/>
)
})
}, [navigation]);
//2. Add BackHandler
useEffect(() => {
const onBackPress = () => {
if (payload) {
navigation.goBack()
navigation.navigate('Notification') //can use this only
return true
} else {
return false
}
}
BackHandler.addEventListener('hardwareBackPress', onBackPress)
return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress)
}, [navigation]);
I can just use navigation.navigate('Notification') to return to Notification but this will cause Detail screen to stay mounted in Stack 2. I want the Stack 2 to return to RewardHome after go back to Notification. Hence I used:
navigation.goBack() //pop screen to RewardHome
navigation.navigate('Notification') //jump to Notification

Related

Make all tab bar buttons unfocused on specific screens

I have a react native app which uses react navigation (V6.x for sure). My app has a main navigator which is a bottom-tabs navigator and contains three screens (tabs). Every one of these screens are stack navigators themselves. Let's say one of my tabs is named Wallet (others are Settings and Transactions). Inside this Wallet screen (which is a stack navigator), i have a HomePage screen, a Receive screen and a Send screen. I want to achieve the following behavior (like below screenshot from designs):
Whenever the user goes to one of Send or Receive screens, i want all the tab bar buttons become unfocused (tab bar is still visibe though). And whenever the user gets back to HomePage screen (or going to Settings or Transactions tab by pressing the corresponding tab button), I want the relevant tab button to get focused again. How can i achieve that with react navigation itself?
(My project is managed by redux, but i prefer not to use state management tools and use react navigation itself)
You can do that, but checking child navigation state inside your TabNavigator's screenOptions.
screenOptions={({ route, navigation }) => {
// get wallet stack route
const walletStack = navigation.getState().routes.find((route) => route.name === 'Wallet');
// get current wallet stack focused screen
const walletRouteName = getFocusedRouteNameFromRoute(walletStack);
const shouldBeUnfocused =
walletRouteName === 'Send' || walletRouteName === 'Receive';
{...}
}
Based on shouldBeUnfocused you can render proper icons and colors. Here is the snack with example code. You can red here about customizing tab bar's appearance.

How to prevent user interaction during screen transition animation?

When navigating between screens using the StackNavigator with a fade transition, a user is able to click during the transition animation and possibly hit a TouchableOpacity on the screen that is being navigated away from. The TouchableOpacity registers the hit and thus the app responds accordingly. This is causing issues for "fast clicking" users where they click a button to navigate to a new screen and immediately click where they think a new button will be, but in reality is clicking a button on the previous screen.
Is there a way to prevent any user interaction during these transition animations? I have tried setting the transition duration to 0 like so:
transitionConfig: () => ({
transitionSpec: {
duration: 0
}
})
but the issue still occurs.
I do not want to disable the animation completely, because it is quick enough for most users and they like the animation.
So in your case you can do several things
You can use React Native Activity Indicator -> View
You can use Overlay Library -> react-native-loading-spinner-overlay -> View GitHub
If you like to make loading like facebook / instagram -> then use react-native-easy-content-loader -> View GitHub
you need to flag screen before navigating away; disabling all touchs.
an easy way would be to have a reusable hook that return a transparent absolute positioned View that cover entier page and a callback to enable it;
so you flow will be; enable this which will overlap whole screen and capture any clicks basically disabling them;
something more like:
function useOverlay(){
const [isVisible, toggle] = React.useState(false);
const Component = React.memo(()=><View style={styles.transparentAbsolute} />,[])
return [toggle, isVisible ? Component : null];
}
then inside your Screen before you call navigate just call toggle
and include Component at top of you screen;
export default function TabOneScreen({ navigation }: RootTabScreenProps<'TabOne'>) {
const [ toggle, component ] = useOverlay();
return (
<View style={styles.container}>
{component}
<Button onPress={()=>{toggle(true); navigation.navigate('Home');} title="go home" />
</View>
);
}

React - Navigating to the same component weird behaviour

I'm breaking my head over this, the behaviour I'm seeing seems weird for me, though it might make sense to some of you.
Consider this component:
const DishPreparation = ({ dish }) => {
const [slideIndex, setSlideIndex] = useState(0)
const sceneRef = useRef(null)
useKeepAwake();
return (
<View style={styles.scene} ref={sceneRef}>
<View flex={0.12} style={{ marginTop:-10 }}>
<ProgressSteps activeStep={slideIndex} activeStepIconBorderColor={colors.lightTan} completedProgressBarColor={colors.lightTan} completedStepIconColor={colors.lightTan}>
{dish.preparationSteps.map((step, index) => (
<ProgressStep removeBtnRow key={index}/> ))}
</ProgressSteps>
......
)
Which is being rendered through another component Dish, that can be navigated to. I have some floating component in my app that allows me to navigate to Dish with a specific dish. If I navigate to a dish through it for the first time, or if I navigate there through other component that doing it by pushing the component to the stack, everything works fine. The weird behaviour is when I'm already inside this component with a specific dish, and then navigate through the floating component to a different dish, it's like the old dish is being kept.
Lets say first dish had 3 elements in dish.preparationSteps, and the second one has 4, then dish.preparationSteps.map(step, index) returns only 3 elements instead of 4, but if I render step.someInfo inside the mapping, then I actually see the new dish values.
How is this happening? I'd expect either 4 elements to be returned, or 3 elements but with the old dish values, how is this mixture happening? Also, dont know if it helps but slideIndex keeps its old value, and doesn't reinitialize to 0.
This is how I navigate through the floating component:
navigation.navigate('Tabs', {
screen: 'Home',
params: { screen: 'Dish', params: { from: '', data: dish } },
})
This is how I navigate to it through other some other component(which works as expected)
navigation.push('Dish', {from: 'DishList', data: item})
If any other code is needed I'll be happy to add it.
When using .navigate instead of .push, navigation will look for that screen name ("Dish") and just change navigation params, without remounting component or opening a new screen with it. Usually you can just use navigation.push (like in your last example), but the problem is that you are trying to navigate from some top-level navigator. You can't use .push there because it will push to the outer navigator which doesn't have "Dish" screen. If you want to add another "Dish" screen on top of the existing one in the nested navigator, you need to navigation.push from the Stack Navigator you want to navigate in; it's only that your floating is not located in that Stack Navigator
First thing you can try is to add a unique key to your screen when navigating, e.g.
navigation.navigate('Tabs', {
screen: 'Home',
params: { screen: 'Dish', key: dish.id, params: { from: '', data: dish } },
})
so that navigation will compare screens not by the name but by the name and key. This will most likely replace existing Dish screen with a new one
Better solution would be to dispatch navigation action that will drill down to the Stack Navigator you want to navigate in, and dispatch StackActions.push there
And even better solution would be not trying to navigate inside inner navigators from outer navigators
Maybe the getId prop is what you're looking for?
From the React Navigation docs:
In a stack navigator, calling navigate with a screen name will result in different behavior based on if the screen is already present or not. If the screen is already present in the stack's history, it'll go back to that screen and remove any screens after that. If the screen is not present, it'll push a new screen.
You could use the getId prop to push a new screen instead. For example, say you have specified a getId prop for Profile screen:
<Screen name={Profile} component={ProfileScreen} getId={({ params }) => params.userId} />
Now, if you have a stack with the history Home > Profile (userId: bob) > Settings and you call navigate(Profile, { userId: 'alice' }) the resulting screens will be Home > Profile (userId: bob) > Settings > Profile (userId: alice) since it'll add a new Profile screen as no matching screen was found.

React-native / redux - how to re-initialize screen via navigation?

I'm developing a react-native / redux app with a bottom-tab-navigator similar to the example at https://reactnavigation.org/docs/en/tab-based-navigation.html#customizing-the-appearance. My screens all connect to a Redux store and display shared data, however I'd like at least one of these screens to ignore the current data in the store and instead re-initialize this data each time it's navigated to (instead of continuing to display the data in whatever state it was last left in).
The screen has a method to do this, but I can't figure out how to call it after the first time the screen is rendered (e.g. from the constructor or componentDidMount() method). I can't call it from the render() method as this causes a "Cannot update during an existing state transition" error.
I need my navigator to somehow cause my HomeScreen.initializeData() method to be invoked each time the Home icon is pressed, but how do I do this?
HomeScreen.js:
initializeData() {
this.props.resetData(initialValue);
}
const initialValue = ...
(resetData() is a dispatch function that re-initializes the Redux store).
Updating state from render() would create an infinite loop. Also, you don’t want to run your state update every time the component re-render, only when the tab button is pressed. This tells me that the proper place to make your state update is some onPress function on the tab button.
So the question now relies on how to implement some onPress function on a tab button. I believe this answer this question:
Is there an onPress for TabNavigator tab in react-navigation?
So I found an answer, it's a little more complicated than might be expected: As Vinicius has pointed out I need to use the tabBarOnPress navigation option, but I also need to make my dispatch function available to this navigation option.
To do this I found I need to pass a reference to my dispatch function (which is available as a property of my screen) into the navigation option, so I've used navigation params to do this and here's what I've ended up with:
HomeScreen.js:
componentDidMount() {
initializeData(this.props);
this.props.navigation.setParams({ homeProps: this.props });
}
export const initializeData = (homeProps) => {
homeProps.resetData(initialValue);
};
const initialValue = ...
AppNavigator.js:
tabBarOnPress: ({navigation, defaultHandler}) => {
const routeName = navigation.state.routeName;
if (navigation.state.params === undefined) {
// no params available
} else if (routeName === 'Home') {
let homeProps = navigation.getParam('homeProps', null);
initializeData(homeProps);
} else if (routeName === ...
...
}
defaultHandler();
}
Notes:
I'm passing props as a navigation param rather than my dispatch function (which also works) as it's more flexible (e.g. it makes all of my dispatch functions available).
initializeData() is called both during construction of HomeScreen (for the first time the screen is displayed) and from the navigation icon (for subsequent displays of the screen).
It's necessary to check that params is defined within the navigation option as it'll be undefined the first time the screen is displayed (as screen construction has yet to occur). This also makes it necessary to call initializeData() during screen construction.

Nagivating to different screen does not call any function

I am using react navigation to create a drawer in my application. I noticed this occurrence when navigating to different screen.
Let's say I have this stack in my app :
Stack A
Stack B
Stack C
When I am at the Stack A and will navigate to Stack B for the first time enter, Stack B will read the componentDidMount() and here I will set a state (which is to connect to rest server to call out data from database).
From the Stack B, I will navigate to Stack C for the first time enter too and it works fine by reading the componentDidMount() too. Then I made some changes from Stack C (example: deleting data) which will affect the data in Stack B.
Now I am from Stack C and navigate back to Stack B (enter for the second time) but it won't read the componentDidMount() anymore. And so, my data will not be updated until I pull down the screen to refresh it.
How should I make the screen be able to read the componentDidMount() every time when enter to the screen?
What you need in this case is to listen to NavigationEvents because the components are already mounted, but didFocus will be called each time the view get the focus.
Here's an example code from the docs:
import React from 'react';
import { View } from 'react-native';
import { NavigationEvents } from 'react-navigation';
const MyScreen = () => (
<View>
<NavigationEvents
onWillFocus={payload => console.log('will focus',payload)}
onDidFocus={payload => console.log('did focus',payload)}
onWillBlur={payload => console.log('will blur',payload)}
onDidBlur={payload => console.log('did blur',payload)}
/>
{/*
Your view code
*/}
</View>
);
export default MyScreen;
This is what stack navigator does, it want again load whole screen.
It just stores everything for you so that when you navigate back everything is there in whatever state you left the screen.
For example, you scrolled to half on particular screen and navigated to other screen,
now you came back and you will find your screen half scrolled where you left.
so it will do nothing when you came back.
Note: If screen is navigated in past and exist in current stack then navigating to screen again will not call any lifecycle methods.
So for your case,
you can pass a method reference to navigation params. and call it before you navigate.
like this,
let say you are in screenB and wanna call a method methodSuperCool=()=>{...} which resides in screenA from which you navigated to current screen.
for this you will have to pass method reference in params when you navigate to screenB from screenA.
this.props.navigation.navigate('screenB',{methodSuperCool:this.methodSuperCool});
//this to be write in screenA
now in screenB before you naviagte to screenA call this,
this.props.navigation.state.params.methodSuperCool() // this can also have params if you like to pass
this.props.navigation.navigate('screenA') // or goBack() method will also work
Navigating back from Stack C to Stack B wont call componentDidMount() as the components were already mounted when Stack B was first created.
you can do is reset the navigation stack when navigating from Stack B to Stack C like this
const stackCAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'StackC' })],
});
dispatching with
this.props.navigation.dispatch(stackCAction);
note going back wont be possible doing this.
alternately you can pass a callback function from Stack B to Stack C to refresh.
Check this link for full answer.