Get navigation props in an independent component React Native - reactjs

My app.js file looks like this
export default class App extends React.Component {
render() {
return (
<Root style={{
flex: 1
}}>
<FcmHandler/>
</Root>
)
}
}
The Root component is where the entire app resides along with all the functionality, the FcmHandler is where I handle functionality related to notifications etc. Within the FcmHandler I have a method that gets a callback when a notification is clicked, inside this callback I need to navigate to a specific screen in the app based on the notification click.
The problem is using the current code above the FcmHandler component never even gets initialized.
If I try something like this
export default class App extends React.Component {
render() {
return (
<View style={{
flex: 1
}}>
<Root/>
<FcmHandler/>
</View>
)
}
}
the FcmHandler component gets called but I do not have any access to navigation props which reside inside the <Root/> component.
The <Root/> component consists of the following
const ArticleStack = StackNavigator(
{
...
}
);
const SettingsStack = StackNavigator({
...
});
export const Root = StackNavigator({
Articles: {
screen: ArticleStack
},
Settings: {
screen: SettingsStack
},
}, {
mode: 'modal',
headerMode: 'none'
});
The basic goal I am trying to achieve is, when a notification is click, irrespective of which screen the app is currently on I should be able to navigate to a particular screen. I do not want to write the navigation code in every screen component that I have, that seems redundant.

You can follow this official guide to create your navigation service. Then use the navigation service in FcmHandler instead of navigation prop. This way there is no need to put FcmHandler as a child of the navigator.
If you are using redux or mobx, it's better to move your navigation state to the store for easier access. For redux, there is an official integration guide. For mobx, you can try this.

For react-navigation users, a really cool way is to create your own Navigation Service
You can initialize your Navigation Service module, during the time initializing your navigation store as mentioned in their docs
<AppNavigator navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.nav,
addListener,
})} />
// Just add another line to config the navigator object
NavigationService.configNavigator(dispatch) <== This is the important part
NavigationService.js
import { NavigationActions } from 'react-navigation'
let config = {}
const configNavigator = nav => {
config.navigator = nav
}
const reset = (routeName, params) => {
let action = NavigationActions.reset({
index: 0,
key: null,
actions: [
NavigationActions.navigate({
type: 'Navigation/NAVIGATE',
routeName,
params,
}),
],
})
config.navigator(action)
}
const navigate = (routeName, params) => {
let action = NavigationActions.navigate({
type: 'Navigation/NAVIGATE',
routeName,
params,
})
config.navigator(action)
}
const navigateDeep = actions => {
let action = actions.reduceRight(
(prevAction, action) =>
NavigationActions.navigate({
type: 'Navigation/NAVIGATE',
routeName: action.routeName,
params: action.params,
action: prevAction,
}),
undefined
)
config.navigator(action)
}
const goBack = () => {
if (config.navigator) {
let action = NavigationActions.back({})
config.navigator(action)
}
}
export default {
configNavigator,
navigateDeep,
navigate,
reset,
goBack,
}
Explanation :
The config initializes the navigator's dispatch object whenever your redux-navigation gets initialzed, therefore you can dispatch any navigation action, wrt the method's present in the Service Component.
Use
NavigationServices.navigate('ScreenName')
Update:
React Navigation now provides a HOC wrapper withNavigation, that passes the navigation prop into a wrapped component.
It's useful when you cannot pass the navigation prop into the component directly, or don't want to pass it in case of a deeply nested child.
Usage is well mentioned in their docs.

After a bit of research, the easiest way I found was to follow their official documentation:
I created a RootNavigation.js file in the ./misc folder;
import * as React from 'react';
export const navigationRef = React.createRef();
export function navigate(name, params) {
navigationRef.current?.navigate(name, params);
}
I imported it into App.js and created a reference to it in the return function:
import React from 'react'
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { navigationRef } from './misc/rootNavigation'; <- navigationRef is imported
…
const Stack = createStackNavigator();
function App() {
return (
<Provider store={store}>
<NavigationContainer ref={navigationRef}> <— reference to navigationRef
<Stack.Navigator>
…
<Stack.Screen
name="Screen"
component={Screen}
options={{
title: “Hello”,
headerLeft: () => <ScreenButton/>
}} />
</Stack.Navigator>
</NavigationContainer>
</Provider>
);
}
export default App
I called it inside the ScreenButton component
import React, { Component } from 'react'
…
import * as RootNavigation from '../misc/rootNavigation'; <—- imported
class RoomButton extends Component {
constructor(props) {
super(props)
}
render() {
return (
<TouchableOpacity onPress={
() => {RootNavigation.navigate( 'RoomSelectorScreen' ) <—- called here
…
</TouchableOpacity>
)
}
}

Related

react-navigation wrap root AppContainer with react Context

I'm looking for the way to manage global state across my react-native app using react-navigation. I tried to implement basic React Context, which I wanted to wrap around the react-navigation's createAppContainer() method but it didn't work.
I ended up wrapping an app container from index.js file using Context's HOC, but it seems like react-navigation has a problem with re-rendering of nested components, when Context's state is changed. I can access my Context from nested Components but they just aren't re-rendered when context state is changed.
My index.js file looks like:
import { AppRegistry } from "react-native";
import App from "./src/App";
import { withAppContextProvider } from "./src/AppContext";
import { name as appName } from "./app.json";
AppRegistry.registerComponent(appName, () => withAppContextProvider(App));
My context class looks like:
// for new react static context API
export const AppContext = createContext({});
// create the consumer as higher order component
export const withAppContext = ChildComponent => props => (
<AppContext.Consumer>
{context => <ChildComponent {...props} global={context} />}
</AppContext.Consumer>
);
// create the Provider as higher order component (only for root Component of the application)
export const withAppContextProvider = ChildComponent => props => (
<AppContextProvider>
<ChildComponent {...props} />
</AppContextProvider>
);
export class AppContextProvider extends Component {
state = {
isOnline: true
};
handleConnectivityChange = isOnline => {
this.setState({ isOnline });
};
componentDidMount = async () => {
NetInfo.isConnected.addEventListener(
"connectionChange",
this.handleConnectivityChange
);
};
componentWillUnmount() {
NetInfo.isConnected.removeEventListener(
"connectionChange",
this.handleConnectivityChange
);
}
render() {
return (
<AppContext.Provider
value={{
...this.state
}}
>
{this.props.children}
</AppContext.Provider>
);
}
}
my App.js file looks like:
const HomeStack = createStackNavigator(
{
Home: HomeScreen,
Cities: CitiesScreen
},
getStackConfig({ initialRouteName: "Home" })
);
const SettingsStack = createStackNavigator(
{
Settings: SettingsScreen
},
getStackConfig({ initialRouteName: "Settings" })
);
export default createAppContainer(
createBottomTabNavigator(
{
Home: HomeStack,
Settings: SettingsStack
}
)
);
CitiesScreen component example:
import { AppContext } from "../AppContext";
class CitiesScreen extends Component {
static contextType = AppContext;
render() {
return (
<View style={styles.container}>
<Text>This value should change on isOnline update: {this.context.isOnline}</Text>
</View>
);
}
}
Now, when I'm accessing Context, from for example CitiesScreen component, I'm currently able to get the value of isOnline state of context but whenever I switch my internet connection (on android emulator) on/off, the context state is changed but the component isn't re-rendered and my shouldComponentUpdate() method isn't triggered. Any help to make this work?
In my case, I downgraded React from 16.8 to 16.5.0, react navigation version is 3.
I'm still investigating but that's a temporary solution for now.

How should the new context api work with React Native navigator?

I created a multiscreen app using React Navigator following this example:
import {
createStackNavigator,
} from 'react-navigation';
const App = createStackNavigator({
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
});
export default App;
Now I'd like to add a global configuration state using the new builtin context api, so I can have some common data which can be manipulated and displayed from multiple screens.
The problem is context apparently requires components having a common parent component, so that context can be passed down to child components.
How can I implement this using screens which do not share a common parent as far as I know, because they are managed by react navigator?
You can make it like this.
Create new file: GlobalContext.js
import React from 'react';
const GlobalContext = React.createContext({});
export class GlobalContextProvider extends React.Component {
state = {
isOnline: true
}
switchToOnline = () => {
this.setState({ isOnline: true });
}
switchToOffline = () => {
this.setState({ isOnline: false });
}
render () {
return (
<GlobalContext.Provider
value={{
...this.state,
switchToOnline: this.switchToOnline,
switchToOffline: this.switchToOffline
}}
>
{this.props.children}
</GlobalContext.Provider>
)
}
}
// create the consumer as higher order component
export const withGlobalContext = ChildComponent => props => (
<GlobalContext.Consumer>
{
context => <ChildComponent {...props} global={context} />
}
</GlobalContext.Consumer>
);
On index.js wrap your root component with context provider component.
<GlobalContextProvider>
<App />
</GlobalContextProvider>
Then on your screen HomeScreen.js use the consumer component like this.
import React from 'react';
import { View, Text } from 'react-native';
import { withGlobalContext } from './GlobalContext';
class HomeScreen extends React.Component {
render () {
return (
<View>
<Text>Is online: {this.props.global.isOnline}</Text>
</View>
)
}
}
export default withGlobalContext(HomeScreen);
You can also create multiple context provider to separate your concerns, and use the HOC consumer on the screen you want.
This answer takes in consideration react-navigation package.
You have to wrap your App component with the ContextProvider in order to have access to your context on both screens.
import { createAppContainer } from 'react-navigation'
import { createStackNavigator } from 'react-navigation-stack'
import ProfileContextProvider from '../some/path/ProfileContextProvider'
const RootStack = createStackNavigator({
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
});
const AppContainer = createAppContainer(RootStack)
const App = () => {
return (
<ProfileContextProvider>
<AppContainer />
</ProfileContextProvider>);
}
https://wix.github.io/react-native-navigation/docs/third-party-react-context/
As RNN screens are not part of the same component tree, updating the values in the shared context does not trigger a re-render across all screens. However you can still use the React.Context per RNN screen component tree.
If you need to trigger a re-render across all screens, there are many popular third party libraries such as MobX or Redux.

How to pass props in StackNavigator

const MainNavigator = StackNavigator({
Home: {
screen: Tabs
},
Collection_Products : {
screen : Collection_Products,
navigationOptions : ({ navigation }) => ({
title : `${navigation.state.params.name.toUpperCase()}`
}),
},
MainProduct : {
screen : (props) => <MainProduct {...props} />,
},
});
export default class MainNav extends React.Component {
constructor() {
super();
this.state = {
checkout: { lineItems: { edges: [] } }
};
}
method1 = (args) => {
}
render() {
return (
<View style={{flex: 1}}>
<MainNavigator checkout= {this.state.checkout} method1 = {this.method1}/>
</View>
);
}
}
I am using react native to build application. I want to pass method 1 to different child components. How can I pass method1 function in MainProduct Component? With the syntax I am using I am only able to pass navigator props to child components.
The accepted answer is outdated for React Navigation v5 and v6
screenProps is no longer available, which caused several problems.
Use react context instead
Quote from react-navigation 5.x,
Due to the component based API of React Navigation 5.x, we have a much better alternative to screenProps which doesn't have these disadvantages: React Context. Using React Context, it's possible to pass data to any child component in a performant and type-safe way, and we don't need to learn a new API!
Alternatively, you may use routeProps
Passing parameters to routes. v6 doc
navigation.navigate('RouteName', { /* params go here */ })
In case you are new to context
Here is how you may use context to pass props.
Example:
import React from 'react'
// Create context outside of your component
// which can be export to your child component
const MyContext = React.createContext()
export default function MyParentComponent() {
const myContext = {
whatIWhatMyChildToKnow: 'Hi, I am your father.',
}
return (
<MyContext.Provider value={myContext}>
// Dont pass your own props to your navigator
// other than RNavigation props
<YourStackNavigator>
...stack logic
</YourStackNavigator>
</MyContext.Provider>
)
}
// access your context value here
function MyChildScreen() {
const { whatIWhatMyChildToKnow } = React.useContext(MyContext)
// will display 'Hi, I am your father.'
return <span>{whatIWantMyChildToKnow}</span>
}
You need to send the props as below. You need to send props name as 'screenProps' literally then only it is send. Yeah, This is little strange. I tried to change the name of the props, it did not get trough.
const propsForAll = {
checkout:/*your checkout data */,
method1: /**Your medhod1*/
}
<View style={{flex: 1}}>
<MainNavigator screenProps={propsForAll}/>
</View>
I think you may want screenProps, from the docs:
const SomeStack = StackNavigator({
// config
});
<SomeStack
screenProps={/* this prop will get passed to the screen components as
this.props.screenProps */}
/>

Could not find "store" in either the context or props when showing modal

This is my start point of the app:
const App = () => (
<Provider store={store}>
<StackNav />
</Provider>
);
AppRegistry.registerComponent('MyApp', () => App);
For the StackNav I have the following configuration:
const routeConfiguration = {
TO_LOGIN: { screen: Login },
TO_HOME: { screen: Home },
};
const stackNavigatorConfiguration = {
headerMode: 'screen',
navigationOptions: {
header: null,
}
};
Inside Login screen I show a modal inside which I cannot use the connect method of react-redux. This is the full error:
Here is how I am trying to use it:
class SignModal extends Component {
}
export default connect()(SignModal);
What can be the reason? Thank you in advanced!
Modal will exist outside the current component hierarchy. The pattern is called a Portal. This means that it cannot access the React context, hence cannot find the Redux store. Currently there is no API in React Native to fix this. You need to explicitly pass the store as a prop.
Probably better to just connect the component which has the Modal, and pass needed values as props to inner component.
Have you tried
export const mapStateToProps = (state) => {
return {
//what you want from the store
}
}
and then
export default connect(mapStateToProps)(SignModal);
This is possible. Use your modal as a view component and create a connected container component.
// components/Modal.js
class Modal extends Component {
render() {
<Modal />
}
}
export default Modal
// containers/ModalContainer
import Modal from "../components/Modal"
const mapState = state => ...
const mapDispatch = dispatch => ...
export default connect(mapState, mapDispatch)(Modal)
Import Modal from "../containers/ModalContainer" when you use it.

How to get current navigation state

I am using React Navigation's Tab Navigator from https://reactnavigation.org/docs/navigators/tab, when I switch between the Tab Screens I don't get any navigation state in this.props.navigation.
Tab Navigator:
import React, { Component } from 'react';
import { View, Text, Image} from 'react-native';
import DashboardTabScreen from 'FinanceBakerZ/src/components/dashboard/DashboardTabScreen';
import { TabNavigator } from 'react-navigation';
render() {
console.log(this.props.navigation);
return (
<View>
<DashboardTabNavigator />
</View>
);
}
const DashboardTabNavigator = TabNavigator({
TODAY: {
screen: DashboardTabScreen
},
THISWEEK: {
screen: DashboardTabScreen
}
});
DASHBOARD SCREEN:
import React, { Component } from 'react';
import { View, Text} from 'react-native';
export default class DashboardTabScreen extends Component {
constructor(props) {
super(props);
this.state = {};
console.log('props', props);
}
render() {
console.log('props', this.props);
return (
<View style={{flex: 1}}>
<Text>Checking!</Text>
</View>
);
}
}
I get props at Dashboard Screen when it renders the component first but I don't get props when I switch the tabs.
I need to get the current Screen name i.e TODAY or THISWEEK.
Your problem is about "Screen Tracking", react-navigation has an officially guide for this. you can use onNavigationStateChange to track the screen by using built-in navigation container or write a Redux middleware to track the screen if you want to integrate with Redux. More detail can be found at the officially guide: Screen-Tracking. Below is a sample code from the guide by using onNavigationStateChange:
import { GoogleAnalyticsTracker } from 'react-native-google-analytics-bridge';
const tracker = new GoogleAnalyticsTracker(GA_TRACKING_ID);
// gets the current screen from navigation state
function getCurrentRouteName(navigationState) {
if (!navigationState) {
return null;
}
const route = navigationState.routes[navigationState.index];
// dive into nested navigators
if (route.routes) {
return getCurrentRouteName(route);
}
return route.routeName;
}
const AppNavigator = StackNavigator(AppRouteConfigs);
export default () => (
<AppNavigator
onNavigationStateChange={(prevState, currentState) => {
const currentScreen = getCurrentRouteName(currentState);
const prevScreen = getCurrentRouteName(prevState);
if (prevScreen !== currentScreen) {
// the line below uses the Google Analytics tracker
// change the tracker here to use other Mobile analytics SDK.
tracker.trackScreenView(currentScreen);
}
}}
/>
);
Check all properties first, like
<Text>{JSON.stringify(this.props, null, 2)}</Text>
Above json array will show you current state of navigation under routeName index i.e.
this.props.navigation.state.routeName
Have you tried to define navigationOptions in your route object?
const DashboardTabNavigator = TabNavigator({
TODAY: {
screen: DashboardTabScreen
navigationOptions: {
title: 'TODAY',
},
},
})
You can also set navigationOptions to a callback that will be invoked with the navigation object.
const DashboardTabNavigator = TabNavigator({
TODAY: {
screen: DashboardTabScreen
navigationOptions: ({ navigation }) => ({
title: 'TODAY',
navigationState: navigation.state,
})
},
})
Read more about navigationOptions https://reactnavigation.org/docs/navigators/
Answer as of React Navigation v6
Depending on whether you want to trigger re-renders on value changes:
const state = navigation.getState();
or
const state = useNavigationState(state => state);
Reference:
https://reactnavigation.org/docs/use-navigation-state#how-is-usenavigationstate-different-from-navigationgetstate

Resources