Nagivating to different screen does not call any function - react-native

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.

Related

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.

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

react native while come back to previous screen the page data is not updated

I did some changes in my second screen and comeback to my first screen the data is not updated. After reloading app the data is updated. How to update the data from second screen to first screen navigation without refresh or reload
What i believe without any code is that if you do navigation.goBack() or navigation.navigate() it doesnt call the api if its in your componentDidMount, what you can try is adding an eventlistener called onFocus so that whenever screen is focused you call that :
like this in your componentDidMount
componentDidMount(){
this.focusListener = this.props.navigation.addListener('didFocus', () => {
// The screen is focused
// Calling action to reset current day index to 1
this.getItineryData();
});
}
Hope it helps

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.

ComponentDidMount fires twice

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