React-native-navigation: [V2] How to set root component for multiple screens (like Provider for Context or Redux)

Created on 13 Jul 2018  ·  19Comments  ·  Source: wix/react-native-navigation

I need all screens to be wrapped inside the same instance of Provider component.

In version 1 of react-native-navigation there was 3rd parameter of registerComponent that was allowing that.

Is it possible in v2?

Example:

In case like this:
https://reactjs.org/docs/context.html#updating-context-from-a-nested-component

Context value is based on root component state. This requires, however, to have only single instance of this root component. When I'm registering component inside react-native-navigation I need to pass this root component everytime (so it's not the same instance), so it have different internal state, so it's not possible to sync context between multiple screens.


Environment

  • React Native Navigation version: 2.0.2410
  • React Native version: 0.56.0
  • Platform(s) (iOS, Android, or both?): iOS
  • Device info (Simulator/Device? OS version? Debug/Release?): Simulator
accepteenhancement 📌 pinned

Most helpful comment

I really would prefer a way set a root "component" with all the providers instead of having to wrap them on every screen, because that would cause multiple instances of each provider, which defeats the purpose of the new context api... :/

All 19 comments

You probably want to take a look at this thread https://github.com/wix/react-native-navigation/issues/3549

Here's my solution. i created a helper function:
```
import React from 'react';
import { Provider } from 'react-redux';

const wrapWithProvider = component, store) => (
() => React.createElement(
Provider,
{ store },
React.createElement(component, null)
)
);

and then when you register your component 

Navigation.registerComponent('HOME', () => wrapWithProvider(Home, store));
```

@abdullah-sr app crashes at first link with your method. Are you sure of it?

I solved it in this way:

const wrapWithToastProvider = Comp => props => (
    <ToastProvider>
        <Comp {...props} />
    </ToastProvider>
);

Navigation.registerComponent('myschool.login', () => wrapWithToastProvider(Login), store, ReduxProvider);

@pie6k @MaxInMoon Redux support was introduced with this PR https://github.com/wix/react-native-navigation/pull/3675
Now, you can use registerComponentWithRedux just as you would with v1.
This issue can probably be closed closed now.

I really would prefer a way set a root "component" with all the providers instead of having to wrap them on every screen, because that would cause multiple instances of each provider, which defeats the purpose of the new context api... :/

@brunolemos 's suggestion would be really handy for things like the native-base StyleProvider: https://docs.nativebase.io/Customize.html#theaming-nb-headref

I would normally wrap the whole app:

<StyleProvider style={getTheme(variables)}>
    <Provider store={store}>
        {app}
    </Provider>
</StyleProvider>

And @sturmenta 's workaround is good but masks any static options() in the component which I find handy for setting the topBar on a screen-by-screen basis.

Like @LordParsley said, when using native base we want to set the theme for the whole app, isn't bad doing it for every component?

Has anybody found a solution to this?

This singleton solution on StackOverflow is the closest thing I could find, but feels very hacky.

Feels like a major flaw in react-native-navigation v2 if we can't use the context api as intended 😞

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
If you believe the issue is still relevant, please test on the latest Detox and report back. Thank you for your contributions.

Hey guys, we've improved supports for this use case. When registering components you can wrap them with providers, additionally you'll need to pass a third argument which is a generator function which returns the actual component.

Context API

Navigation.registerComponent('navigation.playground.ContextScreen', () => (props) => (
  <TitleContext.Provider value={'Title from Provider'}>
    <ContextScreen {...props} />
  </TitleContext.Provider>
), () => ContextScreen);

Redux

Navigation.registerComponent('navigation.playground.ReduxScreen', () => (props) => (
  <Provider store={reduxStore}>
    <ReduxScreen {...props} />
  </Provider>
), () => ReduxScreen);

With this change, Navigation.registerComponentWithRedux is now deprecated and will be removed in the future.
I'm closing the issue but suggestions for improvements and feedback are more then welcome.

@guyca thanks for the tip but I still have the same problem of two instances using Navigation.registerComponent('navigation.playground.ContextScreen', () => (props) => ( <TitleContext.Provider value={'Title from Provider'}> <ContextScreen {...props} /> </TitleContext.Provider> ), () => ContextScreen); the way you provided it with two different screen.

I know this has been closed, but i think you guys should be more elaborate on this... currently learning about this and i found out that Navigation.registerComponentWithRedux is still working, how exactly am i supposed to apply whats below to multiscreens... what components will be in and ReduxScreen, for someone new to react native, i will ask please you should break it down so we get it once... Thank you..

Navigation.registerComponent('navigation.playground.ReduxScreen', () => (props) => (
  <Provider store={reduxStore}>
    <ReduxScreen {...props} />
  </Provider>
), () => ReduxScreen);

@boiy You definitely summarized in one phrase all my frustrations. I'm just learning react and I try for about 2 days now to create a new application where I connect redux and AsyncStorage to my simple app that has a buttonTab navigator and allso I plan to navigate directly between different screens. I guess my simple app is not simple. Can someone point me to a complete up to date example?

Switching from context api to mobx solves all my problems. To register my components and inject the mobx stores i wrote a little helper:

const registerComponentWithStores = (componentID: string, Component: any) => {
  Navigation.registerComponent(componentID, () => (props: Object) => (
    <Component
      { ...props }
      mobxStore1={mobxStore1}
      mobxStore2={mobxStore2}
    />
  ), () => Component)
}

The stores are now available in the props of the registered component: props.mobxStore1, props.mobxStore2. All changes i perform via actions to mutate values in the stores are automatically observed by all other registered screens.

Still don't know how to setup a mobx provider and other provider together (eg. Intl Provider. ThemeProvider)

How to register a root component with these providers and avoid register them individual.

@guyca I'm not sure if I misunderstood something, but as far as I can see in your code, still - every registered component will have different provider 'instance'.

Let's say we have context with some random number.

// each screen will have different 'random' values. I'd like all of them to be wrapped inside the same instance of the provider with the same value across all screens.
Navigation.registerComponent('navigation.playground.ContextScreen', () => (props) => (
  <RandomContext.Provider value={Math.random()}>
    <ContextScreen {...props} />
  </RandomContext.Provider>
), () => ContextScreen);

There are of course ways to synchronize those context values, but the ultimate goal is to have everything wrapped inside the same root providers. And this seems to still be unresolvable as far as I know

The result would be as if you'd

// no matter what, every screen will have the same context value at any given moment
<Provider value={Math.random()}>
  <ScreenA />
  <ScreenB />
</Provider>

Right now my root is:

return (
  <ThemeProvider theme="light">
    <DataProvider>
      <TutorialProvider>
        <DragAndDropProvider>
          <ModalProvider>
            <AuthProvider>
              <ScreenComponent />
            </AuthProvider>
          </ModalProvider>
        </DragAndDropProvider>
      </TutorialProvider>
    </DataProvider>
  </ThemeProvider>
);

I don't really want to have all of them duplicated on every screen as:

  • some of them subscribe to remote changes (which cost me money)
  • technically it's possible I'll have de-synchronized state eg. authProvider will have a different user for short period of time (eg when I'm logging out in some screen and user is used in another)
  • it's slowing down initialization of every screen

I've investigated it a bit more, and it's not possible, as far as I've tried to create a single provider for every screen, which is quite an issue for me.

Imagine a simple case like this:

export function Provider({children}) {
  // let's say we have provider that counts how many times app went to background
  const [timesAppWentToBackground, setTimesAppWentToBackground] = useState(0);

  // every time it happens, we modify inner state of provider
  useAppGoesToBackground(() => {
    setTimesAppWentToBackground(count => count + 1);
  });

  // in real life, we'd create some context for it etc.
  return <>{children}</>
}

It works well if we have only one screen. However, for every new screen, the brand new provider is created with a fresh state of 0 in this case.

This is the simple case and I could keep such count somewhere to be reused later like

let cachedCount = 0;

export function Provider({children}) {
  // let's say we have provider that counts how many times app went to background
  const [timesAppWentToBackground, setTimesAppWentToBackground] = useState(cachedCount);

  useEffect(() => {
    cachedCount = timesAppWentToBackground 
  }, [timesAppWentToBackground]);

  // every time it happens, we modify inner state of provider
  useAppGoesToBackground(() => {
    setTimesAppWentToBackground(count => count + 1);
  });

  // in real life, we'd create some context for it etc.
  return <>{children}</>
}

And this would work. Every new screen would get an 'actual' count. However, in many cases, it's not possible to pass a 'previous state' in such a simple way, especially if it includes some callbacks, etc.

Also, in some cases, the provider is executing some network fetching, etc. In such a case, every screen cause another request, even tho it's not needed.

I consider it serious limitation of react-native navigation.

@pie6k did you find any solution to this? I found that using redux works in that it keeps the state consistent across each and every screen, however, my plan was not to use redux, but if i have to, i might use it with easy peasy for example

@yemi I didn't find any solution for this.

Redux is specific as even if you have multiple providers, you still have a single store, so providers are in sync.

I was experimenting with a single, invisible overlay that is opened all the time and it holds some state that is shared with other screens basing on subscriptions. I had some successes in that, but it was limiting and complicated. It also didn't allow me to use vanilla react things like contexts etc. It was also not possible to pass data between screens in sync way in many cases so I didn't end up using it as it felt way too overengineered

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kiroukou picture kiroukou  ·  3Comments

ghost picture ghost  ·  3Comments

charlesluo2014 picture charlesluo2014  ·  3Comments

yayanartha picture yayanartha  ·  3Comments

nbolender picture nbolender  ·  3Comments