react-navigation squash navigation stack but preserve rendering? - react-native

I'm running in to an issue with react-navigation and a basic StackNavigator where I'd like to navigate back to HomeScreen while preserving the initial render of that component and not "reset" it. It would be compared to multiple goBack() calls until HomeScreen is hit, but in one navigation.
App structure:
- StackNavigator
- HomeScreen
- ChannelScreen
- VideoScreen
There is, however cyclical navigation on ChannelScreen, where you can click another related section and get to another ChannelScreen.
Quickly, the StackNavigator becomes: HomeScreen => ChannelScreen=> ChannelScreen=> ChannelScreen => etc...
We wanted an easy way to get back to HomeScreen, from any ChannelScreen so I employed a basic reset action:
export const resetStackNavigate = (navigation, routeName, params = {}) => {
const resetAction = NavigationActions.reset({
index: 0,
params,
actions: [
NavigationActions.navigate({ routeName })
]
})
navigation.dispatch(resetAction);
}
which works amazingly well with the exception that the reset causes the stack to "reset" so the initial HomeScreen is unmounted and remounted, and all the components/images/etc have to be reloaded.
I've also tried to pass the HomeScreen's state.key (looks like: Init-id-1513113862825-1) down to the ChannelScreen and utilize:
this.props.navigation.goBack(this.props.navigation.state.params.homeScreenKey);
and
this.props.navigation.dispatch(NavigationActions.back({ key: this.props.navigation.state.params.homeScreenKey }));
Both which did nothing.
Is there a way to get back to HomeScreen but preserve the initial screen?

I solved this by caching the first ChannelScreen's this.props.navigation.state.key on navigation to the sub ChannelsScreens:
class SomeComponent extends Component {
_navigateToHome = () => {
const { backKey } = this.props.navigation.state.params;
this.props.navigation.goBack(backKey);
}
_navigateToChannel = () => {
let { backKey } = this.props.navigation.state.params;
if(!backKey) {
backKey = this.props.navigation.state.key
}
navigate('Channel', { backKey });
}
render() {
return (
<View>
{/* the link to another Channel */}
<TouchableOpacity onPress={this._navigateToChannel}>
{/* ... */}
</TouchableOpacity>
{/* Return to HomeScreen */}
<TouchableOpacity onPress={this._navigateToHome}>
{/* ... */}
</TouchableOpacity>
</View>
)
}
}
If you are on the first ChannelScreen, backKey will be null and therefore just .goBack() is called. On each subsequent ChannelScreen the first ChannelScreen's key is passed along allowing .goBack(key) to navigate us back to the HomeScreen.

Related

Component is not showing changes after render in React Native

Newbie to React Native here. I am trying to use Redux for the mobile app for React Native. There are couple of components (app screens). I am choosing the item from one screen (has list of items) and provide it to the next one as param to show its details. When I do it first time, everything is correct. But if I choose another item in the list screen, the second screen does not show the correct item details. Instead it shows the details for the previously selected item.
First screen provides the item and navigates:
this.props.navigation.navigate('DetailsScreen', { item: item.value } );
Second screen:
class DetailsScreen extends Component{
constructor(props) {
super(props);
this._item = this.props.navigation.getParam(‘item’);
}
componentDidMount() {
this.props.dispatchData(this._item);
}
render(){
return (
<View>
<SectionList
sections={this.props.layout}
stickySectionHeadersEnabled={false}
renderItem={({item}) => (
<ListItem
title={item.title}
/>
)}
renderSectionHeader=
{
({section}) => (
<ListItem
bottomDivider={true}
/>)
}
keyExtractor={(item, index) => index}
/>
</View>
);
}
}
const mapStateToProps = state => {
return {
layout: state.detailsReducer.layout,
};
};
const mapDispatchToProps = {
dispatchData: (item) => getLayout(item)
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(DetailsScreen);
The odd thing is that I can see from the logs that new layout (from the newly selected item) is rendered, meaning the render() function in DetailsScreen is called with the correct up-to-date data. But I can not see those changes on the screen. What am I missing? Is this because of navigation saves the previous instance of the screen? Or something else?
Is this because of navigation saves the previous instance of the screen? Or something else?
answer is yes, react navigation, what it does is if you do navigation.navigate('home') to a page , then for the first time it renders it with every lifecycle method, but the second. time it doesnt call componentDidMount since it has been taken from navigation stack. What you can do is try using
this.props.navigation.push('DetailsScreen', { item: item.value } );
It will push the screen to top of stack every time its called.
Hope it helps feel free for doubts
componentDidMount(){
navigation.addListener("willFocus", () => {
// write what you want to be rendered in here
});
}

React navigation undefined params

I'm trying to pass params into a new screen, and implemented it like mentioned here.
I have the following TouchableOpacity button.
<TouchableOpacity
onPress={() => {
this.props.navigation.navigate('SomeScreen', {
title: 'Title',
subTitle: 'Subtitle',
});
}}
>
On the other page (let's call it Somescreen), I have the following:
render() {
const { navigation } = this.props;
const title = navigation.getParam('title');
}
But title above is undefined:
{ params: undefined, routeName: "Somescreen", key: "id-xx" }
My rootStack:
const RootStack = createStackNavigator({
SomescreenA: { screen: SomescreenA },
SomescreenB: { screen: SomescreenB },
}, { headerMode: 'none' });
Why are my params undefined in a new screen?
If you face a situation where your target screen get undefined params, probably you have a nested navigation stack.
Here you have to pass params to the navigate method in this way:
navigation.navigate('Root', {
screen: 'Settings',
params: { user: 'jane' },
});
For more information read this page in the official docs:
https://reactnavigation.org/docs/nesting-navigators/#navigating-to-a-screen-in-a-nested-navigator
In my specific case, I was calling a nested navigator, so I had to manage how send those params to their specific screen, so I did this:
Send params this way...the regular way:
navigation.navigate(
'OrderNavigator',
{itemSelected},
);
Then, from navigator stack I did this:
const OrderNavigator = ({route: {params}}) => {
return (
<Stack.Navigator initialRouteName="Order">
<Stack.Screen name="Order" component={Order} options={{headerShown: false}} initialParams={params} />
</Stack.Navigator>
);
};
And that's it. Then from the screen I got them like this:
const Order = ({route}) => {
const {itemSelected} = route.params;
const {first_name, last_name} = itemSelected;
return (...)
}
I've, unfortunately, encountered cases where navigate(route, params, ...) wouldn't pass the params object, just like you did.
As a workaround, I use the other variant - navigate({routeName, params, action, key}) that you can find here. It always works.
The accepted answer workaround did not work for me, so apparently if you use children to render your component (in screen options) and pass route as a prop, it works
if you are on react navigation v6^ use the useRoute hook to access the params object
const route = useRoute();
useRoute is a hook that gives access to the route object. It's useful when you cannot pass the route prop into the component directly, or don't want to pass it in case of a deeply nested child.
below is an implementation of this
import { useNavigation, useRoute } from '#react-navigation/native';
import { Pressable, Text } from 'react-native';
function Screen1() {
const navigation = useNavigation();
return (
<Pressable
onPress={() => {
navigation.navigate('Screen2', { caption: 'hey' });
}}
>
<Text> Go to Screen 2 </Text>
</Pressable>
);
}
function Screen2() {
const route = useRoute();
return <Text>{route.params.caption}</Text>;
}

React Navigation - How to pass data across different screens in TabNavigator?

I have a TabNavigator, and in each tab is a StackNavigator. Inside the StackNavigator, I have screens. The screens in each Tab do not call each other directly; the TabNavigator handles the screen changes when a tab is pressed.
In the first tab, if the user clicks a button, some data is created. If the user then navigates to the second Tab, I would like to pass this data to the screen in the second Tab.
Here is a demo of the code:
import React from 'react';
import { Button, Text, View } from 'react-native';
import {
createBottomTabNavigator,
createStackNavigator,
} from 'react-navigation';
class HomeScreen extends React.Component {
doIt = () => {
this.props.navigation.setParams({results: ['one', 'two']}); // <--- set data when user clicks button.
}
render() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
{/* other code from before here */}
<Button
title="Set Results"
onPress={this.doIt}
/>
</View>
);
}
}
class SettingsScreen extends React.Component {
render() {
console.log(this.props.navigation); // <--- console out when user clicks on this tab
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Settings</Text>
</View>
);
}
}
const HomeStack = createStackNavigator({
Home: HomeScreen,
});
const SettingsStack = createStackNavigator({
Settings: SettingsScreen,
});
export default createBottomTabNavigator(
{
Home: HomeStack,
Settings: SettingsStack,
},
{
}
);
The this.props.navigation.state.params never gets the data results in the second Tab. There isn't even a key for it, so if I try to access this.props.navigation.state.params.results, it will be undefined.
This is confusing because I thought props.navigation is passed to all screens automatically.
How can I pass data from one screen to another through the TabNavigator, using just react-navigation? I have seen answers that say to use Redux, but I would not like to import another library if all I want is to keep some state across screens in different react navigators.
It may seem that this.props.navigation.state.params is only able to old one parameter? Possibly? Try this:
doIt = () => {
this.props.navigation.setParams({results: 'one'}); // <--- set data when user clicks button.
}
console.log(this.props.navigation.state.params.results);
Setting props did not work when passing data across different tabs. I even tried playing with AsyncStorage, trying to save and retrieve them in different tabs.
I ended up using Redux to save my states, and that has worked well so far.
I came across a similar problem. I had a multi page form that the client insisted on having each step be enclosed in a tab on a tab bar. I used the react navigation createMaterialTopTabNavigator to create the navigator and couldn't find an easy way to pass the form data between tabs.
What I end up doing was using react's Context API and wrapped the tab navigator in a root form container that provides the context value to the navigator and routes inside. Here is how I did it:
Root form container
// MultiScreenForm.js
imports...
import MultiScreenFormNavigator from './MultiScreenFormNavigator'
export const FormContext = React.createContext()
class MultiScreenForm extends Component {
constructor(props) {
super(props)
this.state = {
// formDataHere
formUpdaters: {
onToggleOptIn: this.handleToggleOptIn // example updater method
// other
}
}
}
handleToggleOptIn = () => {
// toggle opt in form data with this.setState
}
render() {
return (
<FormContext.Provider value={this.state}>
<MultiScreenFormNavigator />
</FormContext.Provider>
)
}
}
export default MultiScreenForm
Example form page
// ProfileForm.js
imports...
import { FormContext } from './MultiScreenForm'
class ProfileForm extends Component {
render() {
// FormContext.Consumer uses function as child pattern
return (
<FormContext.Consumer>
{ (context) => (
// our form can now use anything that we pass through the context
// earlier, we passed the root form's state including an updater
<button onPress={context.formUpdaters.onToggleOptIn} />
// ...
)
}
</FormContext.Consumer>
)
}
}
export default ProfileForm
Tab navigator
// MultiScreenFormNavigator.js
imports...
import ProfileForm from './ProfileForm'
import { createMaterialTopTabNavigator } from 'react-navigation'
const MultiScreenFormNavigator = createMaterialTopTabNavigator(
{
Profile: ProfileForm,
// AnotherForm: AnotherForm
},
// { navigator options here... }
)
export default MultiScreenFormNavigator
We then render the MultiScreenForm instead of the tab navigator directly.
This worked for me but I feel there should be an easier way to do this. I hope people who read this can share their approaches.
#tempomax
tried same with AsyncStorage but data came in with a delay.
Sometimes you don't need Redux if your app stays small.
So tried to find a way without Redux.
Here is what I came up with
I hope it's not too late to answer.
Solved it with NavigationEvents and setting params to Route.
The problem with tab is that you can´t pass params to screen because navigation.navigate will be triggered automatically if createMaterialTopTabNavigator is swiped or clicked on non-active TabBar Button.
This can be solved with NavigationEvent like follow.
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 =>
/*
if screen is about to change this will be triggred
In screen 'MyScreen2' you can get it with navigation.params
*/
this.props.navigation.navigate('MyScreen2', { name: 'Brent' })
}
onDidBlur={payload => console.log('did blur',payload)}
/>
{/*
Your view code
*/}
</View>
);
export default MyScreen;
Now you can get the data in MyScreen2
/* 2. Get the param, provide a fallback value if not available */
const { navigation } = this.props;
const itemId = navigation.getParam('name', 'DefaultName');
const otherParam = navigation.getParam('otherParam', 'some default value');
If you are using React Native Navigation Version 5.x with a DrawerNavigation, you can do this using
in screen 1:
<Button
onPress={() => {
this.props.navigation.navigate(<ScreenNameOfDrawerScreen>,
{screen:'<ScreenNameInTabDrawer>',params:{your_json_Data}});
}} />
in screen 2:
............
render() {
if(this.props.route.params!=undefined){
if(this.props.route.params.your_json_Data!=null){
// Use this.props.route.params.your_json_Data. It is your json data.
}
}
return (
..............

Where to initialize data loading with react-navigation

I'm using react-navigation and here is my structure :
The root stack navigator :
export const Root = StackNavigator({
Index: {
screen: Index,
navigationOptions: ({ navigation }) => ({
}),
},
Cart: {
screen: Cart,
navigationOptions: ({ navigation }) => ({
title: 'Votre panier',
drawerLabel: 'Cart',
drawerIcon: ({ tintColor }) => <Icon theme={{ iconFamily: 'FontAwesome' }} size={26} name="shopping-basket" color={tintColor} />
}),
},
...
My structure looks like this :
StackNavigator (Root)
DrawerNavigator (Index)
TabNavigator
MyPage
MyPage (same page formatted with different datas)
...
So my question is, where do I load my data, initialize my application ? I need somewhere called once, called before the others pages.
The first page displayed in my application is the MyPage page. But as you can see, because of the TabNavigator, if I put my functions inside, it will be called many times.
Some will says in the splashscreen, but I'm using the main splashscreen component and I don't have many controls over it.
I thought about my App.js where we create the provider, but I don't think this is a good idea ?
const MyApp = () => {
//TODO We're loading the data here, I don't know if it's the good decision
ApplicationManager.loadData(store);
SplashScreen.hide();
return (
<Provider store={store}>
<Root/>
</Provider>
);
};
What is the good way to do it ?
class MyApp extends Component {
state = {
initialized: false
}
componentWillMount() {
// if this is a promise, otherwise pass a callback to call when it's done
ApplicationManager.loadData(store).then(() => {
this.setState({ initialized: true })
})
}
render() {
const { initialized } = this.state
if (!initialized) {
return <SplashScreen />
}
return (
<Provider store={store} >
<Root />
</Provider>
);
}
}
TabNavigator by default renders/loads all its child components at the same time, but if you set property lazy: true components will render only if you navigate. Which means your functions will not be called many times.
const Tabs = TabNavigator(
{
MyPage : {
screen: MyPage
},
MyPage2 : {
screen: MyPage,
}
}
},
{
lazy: true
}
);
If you use this structure and call fetching data inside of MyPage you can add logic in componentWillReceiveProps that will check is data already in store and/or is it changed before fetching new data. Calling your fetch functions from MyPage gives you the ability to pull fresh data on every page/screen visit or do "pull to refresh" if you need one.
You could also pull initial data in splashscreen time, I would just not recommend pulling all your app data, data for all screens, at that time since you probably don't need it all at once. You can do something like:
class MyApp extends Component {
state = {
initialized: false
}
componentWillMount() {
// if this is a promise, otherwise pass a callback to call when it's done
ApplicationManager.loadData(store).then(() => {
this.setState({ initialized: true })
})
}
render() {
const { initialized } = this.state
if (!initialized) {
return null
}
return (
<Provider store={store} >
<Root />
</Provider>
);
}
}
class Root extends Component {
componentDidMount() {
SplashScreen.hide();
}
...
}
You should do it in App.js or where you initialize your StackNavigator. If I were you, I would put a loading screen, which would get replaced by the StackNavigator structure once the data is ready.
I wouldn't do it in the App because you lose control. Sadly I haven't used react-navigation or redux but I see that the TabNavigator has a tabBarOnPress method, which I would use to trigger the loading. You can load every page data on demand.
https://reactnavigation.org/docs/navigators/tab#tabBarOnPress

React-native-navigation Change state from another tabnavigator

I'm using react-navigation / TabNavigator, is there a way to change the state of a tab from another tab without using Redux or mobx?
Yes you can. It is a little complicated, a little hacky and probably has some side-effects but in theory you can do it. I have created a working example snack here.
In react-navigation you can set parameters for other screens using route's key.
When dispatching SetParams, the router will produce a new state that
has changed the params of a particular route, as identified by the key
params - object - required - New params to be merged into existing route params
key - string - required - Route key that should get the new params
Example
import { NavigationActions } from 'react-navigation'
const setParamsAction = NavigationActions.setParams({
params: { title: 'Hello' },
key: 'screen-123',
})
this.props.navigation.dispatch(setParamsAction)
For this to work you need to know key prop for the screen you want to pass parameter. Now this is the place we get messy. We can combine onNavigationStateChange and screenProps props to get the current stacks keys and then pass them as a property to the screen we are currently in.
Important Note: Because onNavigationStateChange is not fired when the app first launched this.state.keys will be an empty array. Because of that you need to do a initial navigate action.
Example
class App extends Component {
constructor(props) {
super(props);
this.state = {
keys: []
};
}
onNavigationChange = (prevState, currentState) => {
this.setState({
keys: currentState.routes
});
}
render() {
return(
<Navigation
onNavigationStateChange={this.onNavigationChange}
screenProps={{keys: this.state.keys}}
/>
);
}
}
And now we can use keys prop to get the key of the screen we need and then we can pass the required parameter.
class Tab1 extends Component {
onTextPress = () => {
if(this.props.screenProps.keys.length > 0) {
const Tab2Key = this.props.screenProps.keys.find((key) => (key.routeName === 'Tab2')).key;
const setParamsAction = NavigationActions.setParams({
params: { title: 'Some Value From Tab1' },
key: Tab2Key,
});
this.props.navigation.dispatch(setParamsAction);
}
}
render() {
const { params } = this.props.navigation.state;
return(
<View style={styles.container}>
<Text style={styles.paragraph} onPress={this.onTextPress}>{`I'm Tab1 Component`}</Text>
</View>
)
}
}
class Tab2 extends Component {
render() {
const { params } = this.props.navigation.state;
return(
<View style={styles.container}>
<Text style={styles.paragraph}>{`I'm Tab2 Component`}</Text>
<Text style={styles.paragraph}>{ params ? params.title : 'no-params-yet'}</Text>
</View>
)
}
}
Now that you can get new parameter from the navigation, you can use it as is in your screen or you can update your state in componentWillReceiveProps.
componentWillReceiveProps(nextProps) {
const { params } = nextProps.navigation.state;
if(this.props.navigation.state.params && params && this.props.navigation.state.params.title !== params.title) {
this.setState({ myStateTitle: params.title});
}
}
UPDATE
Now react-navigation supports listeners which you can use to detect focus or blur state of screen.
addListener - Subscribe to updates to navigation lifecycle
React Navigation emits events to screen components that subscribe to
them:
willBlur - the screen will be unfocused
willFocus - the screen will focus
didFocus - the screen focused (if there was a transition, the transition completed)
didBlur - the screen unfocused (if there was a transition, the transition completed)
Example from the docs
const didBlurSubscription = this.props.navigation.addListener(
'didBlur',
payload => {
console.debug('didBlur', payload);
}
);
// Remove the listener when you are done
didBlurSubscription.remove();
// Payload
{
action: { type: 'Navigation/COMPLETE_TRANSITION', key: 'StackRouterRoot' },
context: 'id-1518521010538-2:Navigation/COMPLETE_TRANSITION_Root',
lastState: undefined,
state: undefined,
type: 'didBlur',
};
If i understand what you want Its how i figure out to refresh prevous navigation screen. In my example I refresh images witch i took captured from camera:
Screen A
onPressCamera() {
const { navigate } = this.props.navigation;
navigate('CameraScreen', {
refreshImages: function (data) {
this.setState({images: this.state.images.concat(data)});
}.bind(this),
});
}
Screen B
takePicture() {
const {params = {}} = this.props.navigation.state;
this.camera.capture()
.then((data) => {
params.refreshImages([data]);
})
.catch(err => console.error(err));
}