New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to navigate from outside of a react component? #1439

Closed
digitalmaster opened this Issue May 10, 2017 · 48 comments

Comments

Projects
None yet
@digitalmaster

digitalmaster commented May 10, 2017

Haven't been able to figure out a way to navigate from outside react-component.

My use case is that I have a saga that waits for a specific redux action to be dispatched; when triggered I want to navigate to a specific route right from my saga function.

Is this even possible without having to integrate redux and take over state management (trying to keep things simple)?

@digitalmaster digitalmaster changed the title from Is it possible to navigate from outside of a react-component? to Is it possible to navigate from outside of a react component? May 10, 2017

@keesee

This comment has been minimized.

keesee commented May 11, 2017

I'm in the process of evaluating react-navigation and am wondering the same. The app we are starting soon needs several nested navigators so I'd rather not need a bunch of reducers. just for managing the router. The redux solutions I've skimmed on in react-navigation issues seem a bit hacky. A router should just work, and you should be able to dispatch an action from anywhere. I did a quick redux setup as a test and it worked well until I introduced a nested router. Documentation didn't help.

@digitalmaster

This comment has been minimized.

digitalmaster commented May 11, 2017

@keesee Yup.. i'm 100% aligned with your description of the complexity i'm trying to avoid. Ideally there should be a way to dispatch actions directly to the react-navigation internal store; maybe by exposing its internal dispatch fn so that we just trigger navigationActions directly to it? Or is there a reducer example that works (including nested routes)?

@daose

This comment has been minimized.

daose commented May 14, 2017

possible duplicate of #840, the solution I mentioned in that issue does require redux integration (react-navigation redux integration guide)

**note:

Keep in mind that when a navigator is given a navigation prop, it relinquishes control of its internal state. That means you are now responsible for persisting its state, handling any deep linking, integrating the back button, etc.

@digitalmaster

This comment has been minimized.

digitalmaster commented May 15, 2017

Not exactly duplicate... my question specifically asks for a solution not requiring redux integration. Like an external api to trigger navigation actions which affect its internal store.

So i'm assuming the answer is No.

@ericvicenti

This comment has been minimized.

Contributor

ericvicenti commented May 15, 2017

It should be possible to call .dispatch(action) on your top-level navigator, provided you aren't providing your own navigation prop as you would with a redux integration

@unexge

This comment has been minimized.

unexge commented May 24, 2017

you can pass your top-level navigator ref to a service, and dispatch actions from that service.

here is mine:

// App.js

import NavigatorService from './services/navigator';

const Navigator = StackNavigator({ /* ... */ })

class App extends Component {
  // ...

  render(): {
    return (
      <Navigator
        ref={navigatorRef => {
          NavigatorService.setContainer(navigatorRef);
        }}
      />
    );
  }
}
// services/navigator.js
// @flow

import { NavigationActions } from 'react-navigation';
import type { NavigationParams, NavigationRoute } from 'react-navigation';

let _container; // eslint-disable-line

function setContainer(container: Object) {
  _container = container;
}

function reset(routeName: string, params?: NavigationParams) {
  _container.dispatch(
    NavigationActions.reset({
      index: 0,
      actions: [
        NavigationActions.navigate({
          type: 'Navigation/NAVIGATE',
          routeName,
          params,
        }),
      ],
    }),
  );
}

function navigate(routeName: string, params?: NavigationParams) {
  _container.dispatch(
    NavigationActions.navigate({
      type: 'Navigation/NAVIGATE',
      routeName,
      params,
    }),
  );
}

function navigateDeep(actions: { routeName: string, params?: NavigationParams }[]) {
  _container.dispatch(
    actions.reduceRight(
      (prevAction, action): any =>
        NavigationActions.navigate({
          type: 'Navigation/NAVIGATE',
          routeName: action.routeName,
          params: action.params,
          action: prevAction,
        }),
      undefined,
    ),
  );
}

function getCurrentRoute(): NavigationRoute | null {
  if (!_container || !_container.state.nav) {
    return null;
  }

  return _container.state.nav.routes[_container.state.nav.index] || null;
}

export default {
  setContainer,
  navigateDeep,
  navigate,
  reset,
  getCurrentRoute,
};

and then you can use Navigator service everywhere.

@keeleycarrigan

This comment has been minimized.

keeleycarrigan commented Jun 6, 2017

Maybe I'm misunderstanding something. Is it possible to export the initialized NavigationService to use inside a saga or something? How do you use it everywhere?

@unexge

This comment has been minimized.

unexge commented Jun 7, 2017

yes, it just a module. you can import and use everywhere.

import NavigatorService from './services/navigator';

NavigatorService.navigate('Home');
@keeleycarrigan

This comment has been minimized.

keeleycarrigan commented Jun 7, 2017

I see. I was thinking about turning this into a class so I could initialize it with multiple instances of navigators if I wanted. I guess I could also have another object to store my instances and allow access to them. Thanks!

@pietro909

This comment has been minimized.

Contributor

pietro909 commented Jun 12, 2017

Dispatching the action doesn't work for me: it does nothing.
The Navigation is up and running, but when onClose is called I can see the log and nonetheless nothing happens :-(

const Knipster = StackNavigator(
  {
    Landing: { screen: Landing },
  // ....
   }
)
class Launcher extends React.Component {

  onClose(target) {
    console.warn(`navigate to ${target}`)
    this.navigator.dispatch(
      NavigationActions.navigate({
        type: 'Navigation/NAVIGATE',
        target,
       }),
     )
  }

render() {
  const screenProps = { onClose: this.onClose.bind(this) }

  return (
    <Navigator
      screenProps={screenProps}
      ref={navigatorRef => this.navigator = navigatorRef}
    />
  )
}
@pietro909

This comment has been minimized.

Contributor

pietro909 commented Jun 13, 2017

@unexge I sorted it out: as per the docs the action has a different format.

This worked for me:

this.navigator.dispatch(
  NavigationActions.navigate({
    routeName: target,
  }),
)
@sankalpsingha

This comment has been minimized.

sankalpsingha commented Jun 22, 2017

@unexge s answer is the easiest that I have found so for dispatching to a different navigation even from redux actions. I think something like this should be merged into the main library.

@keeleycarrigan

This comment has been minimized.

keeleycarrigan commented Jun 22, 2017

Yea this solution is great. I made the posted solution into a class so I can save different navigator instances and then store them in another object that allows me to access them anywhere. It's given me pretty good control over navigation without having to pass navigators as props all over the place.

@digitalmaster

This comment has been minimized.

digitalmaster commented Jul 10, 2017

@unexge Brilliant!!!!

@taylorgoolsby

This comment has been minimized.

taylorgoolsby commented Jul 13, 2017

@unexge This method does not work if your navigator is mounted with a navigation prop (which is probably because you're using redux integration). For example:

<AppNavigator navigation={addNavigationHelpers({
  dispatch: this.props.dispatch,
  state: this.props.nav,
})} />

If anyone does redux integration as described in the docs, then it won't work.

The reason it doesn't work is because the navigator is considered to be not stateful, and dispatch will not do anything if it is. Here is the code responsible for this behavior in createNavigationContainer.js:

dispatch = (action: NavigationAction) => {
  const { state } = this;
  if (!this._isStateful()) {
    return false;
  }
  const nav = Component.router.getStateForAction(action, state.nav);
  if (nav && nav !== state.nav) {
    this.setState({ nav }, () =>
      this._onNavigationStateChange(state.nav, nav, action));
    return true;
  }
  return false;
};

Is there any reason dispatch cannot defer to this.props.navigation.dispatch? In other words, is there any reason dispatch can't be implemented as is:

dispatch = (action: NavigationAction) => {
  const { state } = this;
  if (!this._isStateful()) {
    return this.props.navigation.dispatch(action);
  }
  const nav = Component.router.getStateForAction(action, state.nav);
  if (nav && nav !== state.nav) {
    this.setState({ nav }, () =>
      this._onNavigationStateChange(state.nav, nav, action));
    return true;
  }
  return false;
};

I've gone ahead and implemented this and did minimal testing, and nothing seems to break. Does anyone know if this is a safe change?


As a workaround, you can still use @unexge method with redux by doing _container.props.navigation.dispatch instead of _container.dispatch.

@dwilt

This comment has been minimized.

dwilt commented Aug 24, 2017

I love the solution that @unexge presented but it seems a little ridiculous that it's this difficult

@MacKentoch

This comment has been minimized.

MacKentoch commented Aug 27, 2017

@unexge solution works nicely. Thank you!

But why does dispatch navigation as docs says does not work to navigate from sub navigators even without redux integration?

The idea of using anywhere in the application (with tons of navigators and sub navigators) just something like:

import { NavigationActions } from 'react-navigation'

const navigateAction = NavigationActions.navigate({
  routeName: 'Profile',
  params: {},
  action: NavigationActions.navigate({ routeName: 'SubProfileRoute'})
})

this.props.navigation.dispatch(navigateAction)

is pretty elegant and concise.

Maybe I'm wrong and did not understand?

@ThaJay

This comment has been minimized.

ThaJay commented Oct 4, 2017

@unexge Would it be wise to make a pull request from your code example?

I agree this is something we should not have to implement ourselves if we only need to navigate 'to' something and 'back'.

The only reason I need this right now is because I have a drawer that is used for more than navigation and it was all working perfectly with react-native-router-flux but then they decide to change everything so I ditched it and implemented react-navigation without redux integration (because that gave me an unreasonable headache.) and this way navigation stays simple like it should be. But then I was unable to open our options page from the drawer.

long story short I have copied this whole navigation module for just one call.

@adrienjt

This comment has been minimized.

adrienjt commented Oct 29, 2017

The NavigatorService solution above or presented earlier offer nice abstractions around react-navigation, but if you:

  • need more flexibility,
  • don't want to maintain/extend yet another service,
  • and/or are OK with spreading the dependency on react-navigation's NavigationActions,

there's a simpler solution: just export/import the navigator reference:

// App.js

const AppNavigator = StackNavigator(myRouteConfigs);

export let navigatorRef;

export class App extends Component {
  componentDidMount() {
    navigatorRef = this.navigator;
  }

  render() {
    return (
      <AppNavigator ref={nav => { this.navigator = nav; }} />
    );
  }
}
// someThunks.js (or anywhereOutsideApp.js)

import {navigatorRef} from 'path/to/App';
import {NavigationActions} from 'react-navigation';

import {someEvent} from './someEvents';
import {someSelector} from './someSelectors';

export const someThunk = () => (dispatch, getState) => {
  dispatch(someEvent()); // this is Redux's dispatch
  const foo = someSelector(getState());
  if (foo) {
    navigatorRef.dispatch(NavigationActions.reset({ // this is react-navigation's dispatch
      index: 0,
      actions: [NavigationActions.navigate({routeName: 'Home'})],
    }));
  }
};
@dorshay6

This comment has been minimized.

dorshay6 commented Jan 17, 2018

Another soultion in here:

import React from 'react';
let navigatorRef = null;

export const screenHOC = (WrappedComponent) => {
    const Component = (props) => {
        navigatorRef = props.navigator;
        return (<WrappedComponent {...props} />);
    }
    Component.navigatorStyle = WrappedComponent.navigatorStyle;

    return Component;
}

export const pushScreen = (payload) => navigatorRef.push(payload);
class App extends Component {
  static navigatorStyle = {
    navBarBackgroundColor: '#2a262c',
    navBarTextColor: '#fff',
    screenBackgroundColor: '#14091a',
  };
 render () {
 }
}
export default screenHOC(App);
@brentvatne

This comment has been minimized.

Member

brentvatne commented Feb 7, 2018

this is a docs issue, moved it to react-navigation/react-navigation.github.io#29 -- thanks everyone for providing answers!

@brentvatne brentvatne closed this Feb 7, 2018

@digitalmaster

This comment has been minimized.

digitalmaster commented Feb 7, 2018

hmm.. Yea, i guess we could just document one of the work arounds we've collected here but I think ideally this should be a feature - not requiring a work around - and we would then of course document that.

That said, if a proper implementation is non-trivial then this could certainly be the documented way to accomplish this.

@i6mi6

This comment has been minimized.

i6mi6 commented Feb 8, 2018

Using react-navigation@1.0.0-beta.11


import { addNavigationHelpers } from 'react-navigation';

class Root extends Component {
  render() {
    const nav = addNavigationHelpers({
      dispatch: this.props.dispatch,
      state: this.props.nav,
    })
    return (
      <StackNavigation navigation={nav}/>
    )
  }
}

const mapStateToProps = state => {
  return { nav: state.nav }
};

export default connect(mapStateToProps)(Root);

then call it in Sagas
yield put(NavigationActions.navigate(...))
or Thunk
dispatch(NavigationActions.navigate(...))

@brentvatne

This comment has been minimized.

Member

brentvatne commented Feb 8, 2018

@digitalmaster - makes sense, feel free to open a RFC to describe how the feature should work: https://github.com/react-navigation/rfcs

I personally would not use this feature so someone else will need to draft the RFC

@nthgol

This comment has been minimized.

nthgol commented Mar 8, 2018

If you are looking to dispatch navigation actions inside a redux action creator, but don't want the headache of the current redux integration API, another workaround, though not great, is to pass the navigation action as a callback:
this.props.myActionCreator(() => this.props.navigation.goBack()); inside your component
export const myActionCreator = (callback) => { callback(); } inside your action creator

@pedrogoes

This comment has been minimized.

pedrogoes commented Apr 12, 2018

I made this solution: https://www.npmjs.com/package/react-native-navigation-actions

@djmalice

This comment has been minimized.

djmalice commented May 8, 2018

@pedrogoes : Thanks. Makes life easy for newbies like me. Works like a charm!!

@murrayee

This comment has been minimized.

murrayee commented May 10, 2018

@taylorgoolsby You are my hero too . Thank you so much for your solution. It saved my day!

@xlcrr

This comment has been minimized.

xlcrr commented May 15, 2018

Sorry but what is

import NavigatorService from './services/navigator';

?

@ukaric

This comment has been minimized.

ukaric commented May 28, 2018

I can confirm that without integrating navigation state into Redux that @pedrogoes package works like charm. I'm dispatching nav actions from action creators with no issue. Great job it is by far easiest solution.

@elirangoshen

This comment has been minimized.

elirangoshen commented Jun 15, 2018

Simply get reference to your store, and use store.disptach along with navigation actions :

import { store } from '../Containers/App' // or from whatever your app exists
import {NavigationActions} from 'react-navigation'

store.dispatch(NavigationActions.navigate({routeName: routeName}))
@giacomocerquone

This comment has been minimized.

giacomocerquone commented Jul 4, 2018

@unexge That service can be considered to emulate the behaviour of navigating through props in react-navigation? Or is it computationally more expensive?
What I mean is that everytime I need to navigate deep in my nested navigators structure, with this service, I'd spawn a navigation action that starts from the root and bubbles down until the bottom one.

Is this good?

@unexge

This comment has been minimized.

unexge commented Jul 4, 2018

@giacomocerquone i don't know about last versions of react-navigation but there should be no overhead.

@giacomocerquone

This comment has been minimized.

giacomocerquone commented Jul 4, 2018

@unexge thanks a lot. Benchmarked naively this with a console log in the component rendered and yes, the same components are rendered using these two ways.

I changed your code though in this way:

function navigateDeep(actions: { routeName: string, params?: NavigationParams }[]) {
  _navigator.dispatch(actions.reduce(
    (prevAction, action): any =>
      NavigationActions.navigate({
        routeName: action.routeName || action,
        params: action.params,
        action: prevAction,
      }),
    undefined,
  ));
}

In this way I'm able to navigate like this:
navService.navigateDeep(['Route1', 'Route2']);
And if I need param I can always throw an object in there.

Ps: can't understand why you preferred a reduceright instead of the plain reduce... I find it obvious that's better to express routes where I want to navigate to from left to right

@uinz

This comment has been minimized.

uinz commented Jul 17, 2018

hey guys.

I using "react-navigation": "^2.6.2"

how can I push (not navigate) same routerName without props.navigation?

@sakhmedbayev

This comment has been minimized.

sakhmedbayev commented Sep 11, 2018

@pedrogoes your package works great but I am getting the error about "the StackNavigator function name is deprecated". I believe, it is because of your package dependency of the older react-navigation version. Do you have plans to maintain this package?

@ayushkamadji

This comment has been minimized.

ayushkamadji commented Sep 22, 2018

Hi fellas, I'm obviously missing something here. How does _container get it's reference navigator outside of App.js (or index.js or wherever setContainer was called)?? Have I misunderstood js modules scope all this time??

@giacomocerquone

This comment has been minimized.

giacomocerquone commented Sep 22, 2018

App.js is just the starting point, from there the navservice stores that reference, so I don't understand what do you mean by "outside of App.js". It is a variable that stays there as long as the code is running, no need to set it again when you import somewhere else that navservice because it's already there form the first time you called setContainer in App.js

@ayushkamadji

This comment has been minimized.

ayushkamadji commented Sep 22, 2018

I was expecting modules to have their own scope, this is common in other languages. I have been using js for well over a year now without ever encountering this particular aspect. For those who have the same confusion I suggest googling for "js modules are singleton". Sorry for rubber ducking on an issue.

@giacomocerquone

This comment has been minimized.

giacomocerquone commented Sep 22, 2018

modules do have their own scope (infact they can be considered as singletons)... I believe you're really off the road. Not having a scope means that it falls down to the global scope, but that's not the case @ayushkamadji

@ayushkamadji

This comment has been minimized.

ayushkamadji commented Sep 23, 2018

Yeah, that was worded poorly, I meant each import statement instantiates its own scope (i.e. not singletons).

@NotTooReact89

This comment has been minimized.

NotTooReact89 commented Nov 1, 2018

@unexge did you figure out the flow type for the navigatorRef and container? Using Object is not recommended flow type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment