ComponentDidMount fires twice - react-native

I have an issue with navigating between two routes. My scenario is as following:
I have 2 routes: Route1 and Route2 - both being siblings to each other.
Let say I am at the Route1, from which I can navigate to Route2 with parameters passed (always). I have investigated buggy behaviour when quickly navigating in the following manner:
Route1 -> Route2 (param: 1) -> Route 1 -> Route 2 (param: 2)
I've placed console logs in the Route2 componentDidMount to see what is the output of the following:
const { navigation } = this.props;
console.log(navigation.state.params.param);
To my surprise, if I navigate quickly, the output for the scenario above will be:
1
1
2
While the expected behaviour is:
1
2
Any idea whats going on?

When you navigate from Route2 to Route1, does it come in from the right or left? It's probably getting mounted twice because react-navigation is fun that way :P
You might also be pressing the button too fast. In that case, disable the button for a few hundred ms after the first click.
class Button extends React.Component {
onPress = () => {
if (this.props.disabled) return;
if (this.canPress) {
this.canPress = false;
this.props.onPress();
setTimeout(() => { this.canPress = true; }, this.props.pressTimeout || 500);
}
}
....

If you are using stack navigator then what react-navigation does is for each navigation.navigate it pushes the route in the stack so in your case stack will fill in this way
STACK [Route1]
STACK [Route2,Route1] . // componentDidMount will be called once printing 1
STACK [Route1,Route2,Route1]
STACK [Route2, Route1, Route2, Route1] // whereas here componentDidMount will be called once for each pushed routes, printing 1 and 2 for both routes in the stack
So there will be two Route2 in the stack with different params.
That is the way react-navigation.
So instead of using navigate everytime, you can use following options as well according to your needs :
pop - go back in the stack
popToTop - go to the top of the stack
replace - replace the current route with a new one

Related

Is it possible to step through a React Navigation stack navigator without specifying route names?

Most of the detail is in the title question. I'm not exactly struggling to navigate between screens, but I'm wondering if push/pop (or something else) can be used to step through a stack without identifying specific routes. So if I have a stack with a bunch of screens, can they have a button with something to the effect of
<TouchableOpacity onPress={() => navigation.push()}>
<Text>Next</Text>
</TouchableOpacity>
Or is this a complete misuse of push? The object here is to be able to change the order of the stack quickly in development without having to go through all of the screens and adjust the button paths. So if the screens are simply named 1, 2, 3, and 4 and I want to move 2 to the end (3 becomes 2, 4 becomes 3), I have to update the next/back button routes for screens 2, 3, and 4. Whereas if they are all coded as "next" or "back", all I do is cut and paste screen 2 after screen 4 in the stack navigator definition. Thanks for your help.
You could get the list of names, find the next name, and then push that. Define a helper like this:
const pushNext = () => state => {
// Get the current route name
const currentRouteName = state.routes[state.index].name;
// Get the index of the current route in the route name list
const currentRouteIndex = state.routeNames.indexOf(currentRouteName);
// Get the next item in the route name list
const nextRouteName = state.routeNames[currentRouteIndex + 1];
if (nextRouteName) {
// If there was a next route, return a push action
return StackActions.push(nextRouteName);
} else {
// Reset to current state for noop or whatever you want
return CommonActions.reset(state);
}
};
Then use it in your components:
navigation.dispatch(pushNext());

Screens not changing after CommonActions.reset even though onStateChange fires with right state

I have two screens in a route:
{ A, B }
When going back from B to A, we can't unmount B or we'll lose it's info. So I'm calling:
export function swapRoutes() {
navigationRef.current?.dispatch(state => {
const routes = state.routes.reverse();
return CommonActions.reset({
...state,
routes,
index: routes.length - 1
});
});
}
This works when going from B back to A. I get { B, A } and the screen shows up.
However, when I do the same thing when I try then to go from A to B again, and call the same method, the onStateChanged fires and shows the right state { A, B } and index 1, but the screen doesn't change, it just has A at the top.
Any idea what could cause the screens not to reflect the state?
Also: these are complex screens so I can't just push another screen A or B during these transitions, one of them is a webview and one of them is a module. All I'm trying to do is swap them without re-rendering. Thoughts?
It probably doesn't work because .reverse() mutates the array, so the array reference doesn't change and React Navigation doesn't think that the array has changed.
You need to make sure not to mutate it (e.g. by spreading it to a new array first):
navigationRef.current?.dispatch(state => {
const routes = [...state.routes].reverse();
return CommonActions.reset({
...state,
routes,
index: routes.length - 1
});
});

What is a difference between navigation.navigate(), navigation.push(), navigation.goBack() and navigation.popToTop() if I go back from page to page?

From this and this, I learned that there is a meaningful difference between navigation.navigate() and navigation.push(). Nevertheless, I am still wondering if I can use navigation.navigate() or navigation.push() instead of navigation.goBack() or navigation.popToTop().
In my case, there is one page, and some parameters are included in the navigation. (i.e, through navigation.navigate(param, {...}) or navigation.push(param, {...}). Once I move to another page, some change in variable happened, and now I would like to send back the param with new data to the first page. I considered doing navigation.navigate(param, {...}) or navigation.push(param, {...}) again as it looks I cannot send back any parameters using goBack() or popToTop(), according to this
I checked this, but I am not 100% sure as I think pages might be stacked a lot if a user does the above actions many times.
Lets take an example
Think you have screens A,B and C in the stack and A is the home screen.
The actual stack will be an object but for easy understanding i'm using a simple array.
When you start the stack will be [A]
When you do a navigate to B the stack will be [A,B]
And if you push C to the stack from B then it will be [A,B,C]
Now all this is common but now if you do a navigate to B from C
then it will unmount C and go back to B and the stack will be [A,B]
If you chose push then it will add a new screen to the stack and stack will be [A,B,C,B] Notice that push always adds a new screen to the stack.
Ignore the push and assume that the stack is [A,B,C]
Now if you do goBack from C then it will pop just like the navigate method and go back to B.
But if you do popToTop it will unmount both C and B and make the stack look like this [A].
The difference is that goBack and popToTop does not pass parameters like navigate and push.
There is a way to achieve the same result of popToTop and goBack using navigate and useNavigationState.
The useNavigationState hook will get you the current navigation state which will have the information of all the screens in the stack. The sample navigation state value would be like this
{
stale: false,
type: 'stack',
key: 'stack-A32X5E81P-B5hnumEXkbk',
index: 1,
routeNames: ['Home', 'Details', 'MyView', 'ExtView'],
routes: [
{ key: 'Home-y6pdPZOKLOPlaXWtUp8bI', name: 'Home' },
{
key: 'MyView-w-6PeCuXYrcxuy1pngYKs',
name: 'MyView',
params: { itemId: 86, otherParam: 'anything you want here' },
},
],
}
As you can see you have the option to use this information to navigate to any screen in the stack. The navigate method can be used like below as well
navigation.navigate({ key: navState.routes[0].key, params: { id: 12 } })
If you use the key 0 then you will be taken to root along with a parameter and it will unmount the screen in the middle.
If you want to go back you can simply do an index - 1 which will give the same effect as goBack
navigation.navigate({ key: navState.routes[navState.Index-1].key, params: { id: 12 } })
So your requirement can be achieved.

How to goBack globally between stacks in React Navigation?

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

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.