React-native-navigation: How to access navigator elsewhere than component?

Created on 6 Apr 2017  路  26Comments  路  Source: wix/react-native-navigation

Is it possible to access navigator somewhere else than in component? For example in react-router you can navigate outside of component like in here: http://knowbody.github.io/react-router-docs/guides/NavigateOutsideComponents.html

acceptediscussion

Most helpful comment

Meanwhile I'm doing the following workaround. I created a new class and exported an instance of it:

class NavigationActionsClass {

    setNavigator(navigator) {
      this.navigator = navigator
    }

    push = (params) => this.navigator && this.navigator.push(params)
    pop = (params) => this.navigator && this.navigator.pop(params)
    resetTo = (params) => this.navigator && this.navigator.resetTo(params)
    toggleDrawer = (params) => this.navigator && this.navigator.toggleDrawer(params)
}

export let NavigationActions = new NavigationActionsClass()

on my MainScreen, I set the navigator on the constructor:

class MainScreen extends React.Component {

  constructor (props) {
    super(props)
    NavigationActions.setNavigator(props.navigator)
  }

...

Now I can import the NavigationActions instance and use it anywhere I want, even on my Sagas:

import { NavigationActions } from '../Navigation'

export function * doSomething() {
  try {
    const obj = yield call(...)
    yield put(...)
    yield call(NavigationActions.resetTo, { screen: 'example.AnotherScreen' })
  } catch(error) {
    yield put(...)
  }
}

All 26 comments

It's a feature implemented on v2.0.0 which is not published yet. Currently not supported.

I'll keep this open to notify when published

I am waiting for this! <3 Any estimates for this?

Don't have an estimate other than "soon"

Meanwhile I'm doing the following workaround. I created a new class and exported an instance of it:

class NavigationActionsClass {

    setNavigator(navigator) {
      this.navigator = navigator
    }

    push = (params) => this.navigator && this.navigator.push(params)
    pop = (params) => this.navigator && this.navigator.pop(params)
    resetTo = (params) => this.navigator && this.navigator.resetTo(params)
    toggleDrawer = (params) => this.navigator && this.navigator.toggleDrawer(params)
}

export let NavigationActions = new NavigationActionsClass()

on my MainScreen, I set the navigator on the constructor:

class MainScreen extends React.Component {

  constructor (props) {
    super(props)
    NavigationActions.setNavigator(props.navigator)
  }

...

Now I can import the NavigationActions instance and use it anywhere I want, even on my Sagas:

import { NavigationActions } from '../Navigation'

export function * doSomething() {
  try {
    const obj = yield call(...)
    yield put(...)
    yield call(NavigationActions.resetTo, { screen: 'example.AnotherScreen' })
  } catch(error) {
    yield put(...)
  }
}

@felipemartim Trying to implement your example without much luck. I have some middleware that's being dispatched incomponentDidMount that starts an application boot process (various middleware such as pulling persisted data, authentication checks etc.), this process is also responsible for dispatching navigation actions based on the out come of those checks. Unfortunately the navigator property of my singleton class is always undefined in my navigation middleware, but not when I call setNav in the constructor of my root component. I'm fairly new to react / react-native / redux world so it's possible that I am over looking something obvious. Any thoughts? This pattern seems pretty straight forward. My guess is I have a race condition somewhere, although I'm not certain how based on my understanding of the React component lifecycle.

@nineohnine

Unfortunately the navigator property of my singleton class is always undefined in my navigation middleware, but not when I call setNav in the constructor of my root component.

That's the point. Your root component's constructor needs to set the navigator singleton before your middleware uses it. componentDidMount executes after the constructor, so it works as intended.

Anyway, after trying this approach on a Tab based app, I realized that each Tab has its own Navigator, and this wasn't a nice workaround anymore. I'm now solving this issue by passing a callback in my redux action, and calling that callback from the middlewares. Maybe this will solve your issue.

@felipemartim just so we are on the same page my current implementation works as follows.

constructor() => // sets navigator w/ setNavigator()

...

componentDidMount() => // dispatches boot middleware

... in middleware ...

yield call(NavigationService.push, { screen });  // NavigationService.navigator is undefined

at the moment a single screen app fits my needs. Thanks for the response.

@nineohnine It should work that way, no idea why it's not working :(

I solved this with handle deep link. You can just path a target screen, method name, and method parameters in the deep link payload and then add a navigation event listener that calls the specified method with the given parameters. Works like a charm.

onNavigatorEvent = ({
    type,
    link,
    payload,
}: {
    type: string,
    link: string,
    payload?: { target: string, method: string, params: Object },
}) => {
    if (type === 'DeepLink') {
        // handle events with a method payload
        if (payload && payload.target === this.key) {
            this.props.navigator[payload.method](payload.params);
        }
    }
};

Don't forget to register the method in the constructor. I wrap my screens with a higher order component to inject this everywhere.

You can now speak to each screen with

Navigation.handleDeepLink({
    link: '*',
    payload: {
        target: 'showtimes',
        method: 'push',
        params: { screen: 'movie' },
    },
});

@MrLoh Can you deeplink to a screen that's not yet on the stack? I am trying to push a new screen on to the stack, but its onNavigatorEvent callback is not being called.

@ywongweb that sounds confusing. In a tab based app you only ever push on the tabs and then the screens stack up on top of them.

But yes you can only push after your screen has registered the callback. I handle this through redux. On app start if I have a route I need, I'll put it in a queue (just saving a string description to my redux) then when a callback has been registered I notice the store. This screenReady action has a side effect in which it looks up wether there are any pending routes for it. If so, I call the deeplink and delete it from the queue.

It's quite complicated. But it works. If you're using redux as well, I can send you some code. I use this stuff on app start to handle incoming and persisted links.

Of course this is all just for the app start. Once your app has started any screen you can push on should be registered and you can safely send deep links around as you wish. Only the app start is a pain because we need to make sure everything is registered before we talk to it

@MrLoh
I have a single screen app, sorry I wasn't clear on this before.

My app starts at screen A and when the user press a button it calls a saga. Inside that saga I want to trigger a push screen B using your DeepLink method. But the deeplink call doesn't seem to do anything because screen B at this point has not been instantiated(I added a console.log on screen B's constructor function and it's not called)

Both screens are registered using Navigation.registerComponent(...)

I've only build a tab based app, so not sure about a single screen app. But on a tab app, if I want to push screen A on my tab O I call the push on the tab O and if I than want to push B ontop of A I call push on O again and when I trigger a pop on B it goes back to A and then a pop on A goes back to the home screen O.

also did you register the onNavigationchange in your constructor with
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);

Don't know if this would work but, what about passing:

  • navigatorID
  • navigatorEventID
  • screenInstanceID
    from this.props.navigator on the view component to the navBarCustomView component and initialize a new navigator inside the navBarCustomView component?
class MyViewComponent extends Component {
 constructor(props) {
   super(props)
    this.props.navigator.setStyle({
      navBarCustomView: 'MyCustomView',
      navBarCustomViewInitialProps: {
        navigatorID: this.props.navigator.navigatorID,
        navigatorEventID: this.props.navigator.navigatorEventID,
        screenInstanceID: this.props.navigator.screenInstanceID,
      }
    });
 }
}

import { Navigator } from 'react-native-navigation';

class MyCustomView extends Component {
    constructor(props) {
        super(props);
        this.localNavigator = new Navigator(this.props.navigatorID, this.props.navigatorEventID, this.props.screenInstanceID);
    }
}

@DanielZlotin since 2.0 is now available (although still WIP), is there any update on this issue?

@GedoonS what鈥檚 your usecase? Most things are already possible with v1.

I'd need a reference to the navigator outside a component, to use with redux+saga. I'm trying to integrate this to an existing project where the control of the navigation was outside the components in a separate state logic built on redux and saga. Components would just dispatch actions like this button was pressed, and then state logic would push or reset navigator accordingly. Now I'm not sure if I should hack something or just rebuild the whole thing with a different approach. To some degree this is a smart component vs dumb component thing, because I'd have to move some of the UI logic inside the components. I'm now trying to decide how to proceed. It may be that I've just missed something crucial in rn-navigation that already makes this possible. I only started using it this week.

@GedoonS I have a very similar setup controlling the routing state from redux via sending deep links and wrapping each screen in a responder. See my earlier comments:
https://github.com/wix/react-native-navigation/issues/993#issuecomment-327397224

I鈥檓 also tracking the whole navigator state in redux. I can share my setup sometime, but it has a bunch of specifics. Always wanted to turn it into a package but it would need significantly more work which I don鈥檛 have the time for currently.

@MrLoh that looks pretty good actually. I'm gonna need deep links anyway so I think I'll follow your example. Thanks!

The original issue has been answered so I'll close this issue.

@felipemartim How to implement your solution for tab based app?

@rendomnet like this:

// Somewhere on my screen component
dispatch(actionCreator(_goToScreen))

_goToScreen = () => {
  this.props.navigator.push({
    screen: 'example.screen'
  })
}

// in my saga, that listens to this particular action being dispatched
export function * saga ({ callback }) {
  try {
  ...
    if (callback) {
      yield call(callback)
    } 
  } catch (error) {
    ...
  }
}

I imagine you are familiar with using redux and redux-saga, so the actual implementation details should be clear to you.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

EliSadaka picture EliSadaka  路  3Comments

viper4595 picture viper4595  路  3Comments

bdrobinson picture bdrobinson  路  3Comments

yayanartha picture yayanartha  路  3Comments

birkir picture birkir  路  3Comments