45

NOTE: This query is for react-navigation 5.

In react navigation 4 we could pass a function as a param while navigating but in react navigation 5, it throws a warning about serializing params.

Basically, what I am trying to do is, navigate to a child screen from parent screen, get a new value and update the state of the parent screen.

Following is the way I am currently implementing:

Parent Screen

_onSelectCountry = (data) => {
    this.setState(data);
};
.
.
.

<TouchableOpacity
    style={ styles.countrySelector }
    activeOpacity={ 0.7 }
    onPress={ () => Navigation.navigate("CountrySelect",
        {
             onSelect: this._onSelectCountry,
             countryCode: this.state.country_code,
        })
    }
>
.
.
.
</TouchableOpacity> 

Child Screen

_onPress = (country, country_code, calling_code) => {
    const { navigation, route } = this.props;
    navigation.goBack();
    route.params.onSelect({
        country_name: country,
        country_code: country_code,
        calling_code: calling_code
    });
};
Ayan
  • 2,738
  • 3
  • 35
  • 76

4 Answers4

34

Passing a callback through react native navigation params is not recommended, this may cause the state to freeze (to not to update correctly). The better solution here would be using an EventEmitter, so the callback stays in the Screen1 and is called whenever the Screen2 emits an event.

Screen 1 code :

import {DeviceEventEmitter} from "react-native"

DeviceEventEmitter.addListener("event.testEvent", (eventData) => 
callbackYouWantedToPass(eventData)));

Screen 2 code:

import {DeviceEventEmitter} from "react-native"

DeviceEventEmitter.emit("event.testEvent", {eventData});

useEffect(() => {
return () => {
    DeviceEventEmitter.removeAllListeners("event. testEvent")
  };
}, []);
Jhnsbrst
  • 318
  • 1
  • 14
George Fean
  • 828
  • 10
  • 16
  • 1
    import {DeviceEventEmitter} from "react-native" – Engr.Aftab Ufaq Jun 03 '21 at 06:08
  • 1
    It's better to remove the listener in the screen 1 too – Muaz Nov 21 '21 at 17:22
  • Doesn't this lead to more dirty code in your app, I imagine you'd end up with lots of event emitter and as a consumer of a page, you wouldn't know which emitter to subscribe to where as having an explicit parameter give the consumer of your API more idea on what they can listen to. – Vincent Paing Jan 09 '23 at 08:45
27

Instead of passing the onSelect function in params, you can use navigate to pass data back to the previous screen:

// `CountrySelect` screen
_onPress = (country, country_code, calling_code) => {
  const { navigation, route } = this.props;
  navigation.navigate('NameOfThePreviousScreen', {
    selection: {
      country_name: country,
      country_code: country_code,
      calling_code: calling_code
    }
  });
};

Then, you can handle this in your first screen (in componentDidUpdate or useEffect):

componentDidUpdate(prevProps) {
  if (prevProps.route.params?.selection !== this.props.route.params?.selection) {
    const result = this.props.route.params?.selection;

    this._onSelectCountry(result);
  }
}
satya164
  • 9,464
  • 2
  • 31
  • 42
  • are the `,`s in `this.props.route,params?.selection` intentional in the `componentDidUpdate`? – Ayan Feb 07 '20 at 13:59
  • No, It's a typo – satya164 Feb 07 '20 at 14:03
  • Ok. I will try it out and let you know. One more thing which I am curious, with the above snippet as you showed, what will happen, if the user navigates to the child screen, selects a country which navigates the user back to the parent screen and then the user presses the hardware back button? Will he be taken back to the child screen? – Ayan Feb 07 '20 at 14:10
  • 1
    No, navigate acts similar to goBack if you already visited that screen – satya164 Feb 07 '20 at 14:11
  • I had another question, if you have some time I could use some help here: https://stackoverflow.com/q/59799843/3697102 – Ayan Feb 07 '20 at 16:36
  • @satya164 How do you use useEffect to get passed back results? Thank you! – echo Sep 04 '20 at 21:04
  • @echo did you find an answer to your question? – Keselme Oct 08 '20 at 07:43
  • @satya164 if it's possible to open `CountrySelect` screen from multiple screens, how can you pass the changed value to the exact previous screen? – rener172846 Jan 26 '21 at 18:11
  • @rener172846, how did you solve this? I also want to create reusable screens. – Mr.B Sep 02 '23 at 09:05
3

There is a case when you have to pass a function as a param to a screen.

For example, you have a second (independent) NavigationContainer that is rendered inside a Modal, and you have to hide (dismiss) the Modal component when you press Done inside a certain screen.

The only solution I see for the moment is to put everything inside a Context.Provider then use Context.Consumer in the screen to call the instance method hide() of Modal.

Kacper Madej
  • 7,846
  • 22
  • 36
Alex R
  • 39
  • 1
0

The reason it throws a warning is partly because passing functions in navigation params clutters the address bar and causes errors when running the app as a web app with react-native-web with linking enabled. However, you're right that if you're just developing for mobile it'll work fine, it's just bad style (see https://reactnavigation.org/docs/params/#what-should-be-in-params).

  • Note how when myFunc passed through params is just a bunch of unreadable garbage https://localhost:19006/My%20App?dataId=20&myFunc=function%20onGoBack%28%29%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20_this2.fetchAssets%280%2C%20_this2.state.searchTerm%2C%20_this2.state.selectedFilterOption%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D

    <NavigationContainer linking={{ enabled: true }}

  • This error occurs when you try refreshing the page with a function in navigation params TypeError: _this2.props.route.params.myFunc is not a function

You could use a library like DeviceEventEmitter to avoid this, but it's pretty easy to do without adding another redundant library.

// In App.js
import React, { Component } from 'react'
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import Facilities from './pages/facilities';
import FacilityDetails from './pages/facility-details';

export default class App extends Component {
  pageData = {};

  state = {

  }

  render() {
    const Stack = createNativeStackNavigator();

    return (
      // Setting linking prop to enabled makes the screen and any props encoded in the browser URL on web.
      <NavigationContainer
        linking={{ enabled: true }} 
      >
        <Stack.Navigator >
          <Stack.Screen name='Facilities'>
            {(props) => <Facilities {...props} setPageData={this.setPageData} />}
          </Stack.Screen>
          <Stack.Screen name='Facility Details'>
            {(props) => <FacilityDetails {...props} getPageData={this.getPageData} />}
          </Stack.Screen>
        </Stack.Navigator>
      </NavigationContainer>
    );
  }

  setPageData = (dataName, data) => {
    this.pageData[dataName] = data;
  }

  getPageData = () => {
    return this.pageData;
  }
}

So if we want to pass a function in facilities.js to facility-details.js and call it, we just need to call setPageData in facilities before navigating and then getPageData in facility details:

// In facilities.js
this.props.setPageData('myFunc', this.myFunc);
this.props.navigation.navigate('Facility Details');

// In facility-details.js
this.props.getPageData().myFunc();
MoxJet
  • 11
  • 1