React-native-navigation: Plan for redux support in v2? (HOC support)

Created on 5 Aug 2017  Â·  57Comments  Â·  Source: wix/react-native-navigation

I'm looking to switch from v1 to v2, but the first thing I noticed was that when registering containers, you can no longer pass in the store and Provider. Is this in the roadmap?

acceptebug v2

Most helpful comment

I understand the issue better now. So we are having a problem with how react works, it's a problem with supporting any HOC. The problem is we added useful and easy to use API (static options, additional lifecycle methods) but that comes at the price of having to delegate those methods from the HOC, because that's the one being registered. We have no access to the underlying "screen" component.

This is not easy to fix, at least not extensibly (the example above by @yusufyildirim is a good solution but it already breaks because we added another method onSearchBarUpdated).

Reopening the issue until we figure out the best solution. This is not a redux issue, it's supporting any HOC issue.

All 57 comments

I don't think it's necessary to add any specific support for redux or any other data flow library, all you need to do is wrap your screens in a higher order component <Provider store={store}/>.

I automatically pass all props through to the inner implementation of Container so it should just work.

Open an issue if you see a specific problem with redux

@DanielZlotin Would you please post an example of how to accomplish this with v2? I'm having a hard time implementing redux with the new v2 structure of registerContainer

You can look at the integration test with redux here
https://github.com/wix/react-native-navigation/tree/v2/integration/redux it
provides a example.
If you encounter a specific problem please report

On Sep 21, 2017 17:35, "Erik" notifications@github.com wrote:

@DanielZlotin https://github.com/danielzlotin Would you please post an
example of how to accomplish this with v2? I'm having a hard time
implementing redux with the new v2 structure of registerContainer

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/wix/react-native-navigation/issues/1642#issuecomment-331175942,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AGWgj6KHAqa2DiD8A4gJOVHyisVRkLphks5sknQqgaJpZM4OuQ6B
.

Is there example how to register containers with redux?

I tried to do this

Navigation.registerContainer('screen.Test', () => { return (props) => { return ( <Provider store={store}> <Screen {...props}/> </Provider> ) } });

but it fails

Cannot read property 'didDisappear' of undefined
Cannot read property 'didAppear' of undefined

This should work:

  Navigation.registerContainer('FitHelper.MyScene', () => {
    return class MyScene extends React.Component {
      render () {
        return <Provider store={store}><OtherSceneContent /></Provider>
      }
    }
  })

Under the hood, Navigation's component registry wraps what you return from creator to another component that gets ref from original component, so you need full subclass of React.Component here or refs will not work.

@vovkasm it works, but is there a nicer, cleaner solution for this. I have like 11 containers. And passing it for every looks like overkill... or I am not getting it

You always can create HOC... something like this (not tested):

function sceneCreator (sceneComp, store) {
  return () => {
    class SceneWrapper extends React.Component {
      render () {
        return <Provider store={store}>{React.createElement(sceneComp)}</Provider>
// may be this also works: return <Provider store={store}><sceneComp/></Provider>
      }
    }
  }
}

And use as:

Navigation.registerContainer('screen.Test', sceneCreator(TestScene, store))

Or you mean runtime overkill? You anyway should pass react context (whats Provider do) to underling component hierarchy. As I understand RNN creates totally new hierarchy for every navigation container, so it is not very different from pass store one time for whole app (without RNN). If this ever will slowdown, it will be caused by another things... not Provider comp.

thank you a lot, I meant lines of code overkill :D but yeah you are correct I can use function like that one.

Vladimir is correct. The only other option is to put this exact code into
the library, so it's exactly the same. If we support redux we have to
support every other state management under the sun..

On Dec 29, 2017 19:53, "Hristo Hristov" notifications@github.com wrote:

thank you a lot, I meant lines of code overkill :D but yeah you are
correct I can use function like that one.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/wix/react-native-navigation/issues/1642#issuecomment-354478447,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AGWgj_--cgcQkIUDqKLJizD41rQD0W3dks5tFScNgaJpZM4OuQ6B
.

@vovkasm I had written my own HOC for redux with RNN v2 because I found that the props and life cycle methods (didAppear, didDisappear, etc) were accessible inside of the HOC class (SceneWrapper in the above example) but not within the actual redux component (screneComp in the above).

Is that suppose to be handled by RNN's wrapper? My HOC looks very similar to RNN's ContainerWrapper.js

@michael-gillett Note, I didn't design this library interface.

Yes, there is code duplication :-)
I think, redux can't be used inside RNN because it is not the only state management library in the world (see Daniel's answer).

But on the other hand, you have a liberty to implement different logic and code structure in your HOC and make something different with RNN root props and life cycle methods, e.g. you can pass this info into redux state.
I did not do it myself, so no code samples, sorry.

@vovkasm Fair enough! thanks for the response

My HOC method works fine for my use case, it would be nice to be able to have the library handle it internally, but I understand Daniel's point about having to support all state management systems

@michael-gillett did you get the lifecycle methods working in the components? I'm running into the same issue where the methods are only available in my wrapper.

@stoerebink Yep what I ended up having was more or less the each same lifecycle code that the built in wrapper had inside of my wrapper. That way when the lifecycle methods get triggered in my wrapper I just call the appropriate lifecycle method of my actually component off of its ref.

Check out lib/src/components/ComponentWrapper.js to see what I'm talking about

@michael-gillett can you share code of your hoc? And when do you apply it?

@tbazko Sure! I apply it when I register my screens like @vovkasm above in this thread and here is my actual component wrapper code

import React, { Component } from 'react';
import { Linking, SafeAreaView, StyleSheet } from 'react-native';
import Navigation from 'react-native-navigation';
import { Provider } from 'react-redux';

export function registerContainerWithRedux(
  containerName,
  comp,
  store,
  Provider,
) {
  const generatorWrapper = function() {
    const InternalComponent = comp;

    return class Scene extends Component {
      constructor(props) {
        super(props);
      }
      render() {
        return (
          <Provider store={store}>
            <InternalComponent
              ref="child"
              {...this.props}
            />
          </Provider>
        );
      }

      didAppear(id) {
        instance = this.refs.child.getWrappedInstance();
        if (instance && instance.didAppear) {
          instance.didAppear(id);
        }
      }

      didDisappear(id) {
        instance = this.refs.child.getWrappedInstance();
        if (instance && instance.didDisappear) {
          instance.didDisappear(id);
        }
      }

      onNavigationButtonPressed(id) {
        instance = this.refs.child.getWrappedInstance();
        if (instance && instance.onNavigationButtonPressed) {
          instance.onNavigationButtonPressed(id);
        }
      }
    };
  };

  registerContainer(containerName, generatorWrapper);
}

function registerContainer(containerName, generator) {
  Navigation.registerComponent(containerName, generator);
}

export default registerContainerWithRedux;

@michael-gillett Thanks! Figured out as much, also using refs now.

I had to extend @vovkasm 's HOC like this in order to make it work:

function sceneCreator(sceneComp, store, thirdparam) {
  class SceneWrapper extends React.Component {
    static options = {
      ...sceneComp.options,
    }

    render() {
      return (
        <Provider store={store}>
          {React.createElement(sceneComp, this.props)}
        </Provider>
      )
    }
  }
  return SceneWrapper
}

@michael-gillett Do you have any more example code on how to use / implement redux in RNN v2?

I tested your HOC, but I must be doing something wrong as I am getting this error:

Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

Check your code at registerContainerWithRedux.js:20.
in Scene (at ComponentWrapper.js:52)
in WrappedComponent (at renderApplication.js:35)
in RCTView (at View.js:71)
in View (at AppContainer.js:102)
in RCTView (at View.js:71)
in View (at AppContainer.js:122)
in AppContainer (at renderApplication.js:34)

The code on line 20 is:

<Provider store={store}>

Also:

Unhandled JS Exception: Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

Check the render method of Scene.

Figured it out. I got confused because you already imported Provider so I wasn't using that param when calling the function.

This logic works fine, but how do you manage navigation state?

e.g. Opening a root of ComponentA which pushes a screen of ComponentB; if ComponentB changes the state, the application reloads back to the root of ComponentA (whereas it should stay on ComponentB, it seems a full reload is performed).

I am using the suggested function from @vovkasm, however in order to get it to work, you need to return it:

  sceneCreator(sceneComp, store) {
    return () => {
      return class Wrapper extends React.Component {
        render() {
          return (
            <Provider store={store}>{React.createElement(sceneComp)}</Provider>
          )
        }
      }
    }
  }

Use it like this:

  registerScreens(store) {
    Navigation.registerComponent(
      'screen.id',
      this.sceneCreator(SomeScreen, store)
    )
  }

Answers above sure helped, but I was still facing issues with implementing screen life cycle. Solution provided by @michael-gillett is helpful, but does not solve the issue either (at least in my case).

At first, methods 'didAppear' and 'didDisappear' seem wrong. They could have never been called. According to RNNv2 Screen Lifecycle and _ComponentWrapper.js_, componentDidAppear and componentDidDisappear are used instead.

Another hinder in my way was the following error:
To access the wrapped instance, you need to specify { withRef: true } in the options argument of the connect() call.

Working solution for me is this:

screens/index.js

import registerContainerWithRedux from '../config/registerContainerWithRedux';
import { Provider } from "react-redux";
import store from '../config/store';

import LoginScreen from './LoginScreen';

export const registerScreens = () => {
  registerContainerWithRedux('navigation.LoginScreen', LoginScreen, store, Provider);
}

registerContainerWithRedux.js

import React, { Component } from 'react';
import { Navigation } from 'react-native-navigation';
import { Provider } from 'react-redux';

export function registerContainerWithRedux(containerName, comp, store, Provider)
{
  const generatorWrapper = function() {
    const InternalComponent = comp;

    return class Scene extends Component {
      constructor(props) {
        super(props);
      }

      static options = {
        ...comp.options,
      }

      render() {
        return (
          <Provider store={store}>
            <InternalComponent
              ref="child"
              {...this.props}
            />
          </Provider>
        );
      }

      resendEvent = (eventName, params) =>
      {
        instance = this.refs.child.getWrappedInstance();
        if (instance && instance[eventName]) {
          instance[eventName](params);
        }
      }

      componentDidAppear() {
        this.resendEvent('componentDidAppear');
      }

      componentDidDisappear() {
        this.resendEvent('componentDidDisappear');
      }

      componentWillUnmount() {
        this.resendEvent('componentWillUnmount');
      }

      componentWillReceiveProps(nextProps) {
        this.resendEvent('componentWillReceiveProps', nextProps);
      }

      onNavigationButtonPressed(buttonId) {
        this.resendEvent('onNavigationButtonPressed', buttonId);
      }
    };
  };

  Navigation.registerComponent(containerName, generatorWrapper);
}

export default registerContainerWithRedux;

And finally, in LoginScreen.js, you need to call connect() like this:

connect(mapStateToProps, null, null, {"withRef" : true})(LoginScreen);

However, I find this not to be the final solution either. For example, I am unable to get events like componentWillMount to work, as refs are not ready at that state. The error I am getting is:
TypeError: Cannot read property 'getWrappedInstance' of undefined. Maybe someone could help out?

Overall, I wish React Native Navigation team came up with more friendly solution to redux users. Doing all of this nasty stuff is pain. Out of box feature for this would be just delightful. At its current state, I wonder it is production ready.

I have worked on a fix on a fork of my own: https://github.com/nSimonFR/react-native-navigation/commit/5322ebf84786989714d02a6c024705b493cdbb35

I can use the Api just like in V1:

Navigation.registerComponent(containerName, generatorWrapper, store, Provider);

But connect needs the withRef option in every registered container:

connect(mapStateToProps, mapDispatchToProps, null, { withRef: true })(MyContainer);

It fits my needs as componentWillMount, componentDidAppear and other methods seems to be working properly.

I can make a PR for this, but i'm not sure about code coverage and my TS skills...

why this issue is closed? anyone found working solution?

+1

Hey,

We are facing the same issue. We created a workaround thanks to helpful comments.

Is there any plan for supporting redux officially in v2?

Hi,

I'm facing the same issue, used the registerContainerWithRedux.js and worked fine until now. Now I'm getting the following error:

img_1937

Which is related to these lines of code on registerContainerWithRedux.js.

instance = this.refs.child.getWrappedInstance();
        if (instance && instance[eventName]) {
          instance[eventName](params);
}

@jaumevn just declare the variable adding const or var in front of it
```js
const instance = this.refs.child.getWrappedInstance();
if (instance && instance[eventName]) {
instanceeventName;
}

Why this issue closed?

thanks @cayasso this is working!

Hey guys V2 looks great! I tried to get redux going through all the different suggestions above but no luck. If anyone has a working solution for this, I would be extremely grateful.

Also want to add a +1 for keeping the same register support from V1 in V2

componentDidAppear is still not working with @Miras4207 solution. Can someone confirm "componentDidAppear" working with redux?

any final solution?

@Miras4207
If we connect our component in this way:
export default connect(mapStateToProps, null, null, {"withRef" : true})(LoginScreen);
Then in your method registerContainerWithRedux we can't access to options like this:
...comp.options so any idea how to get options?

Here is my fully working version HOC.

import React, { Component } from 'react';
import { Provider } from 'react-redux';

export default function reduxHOC(Scene, store) {
  return class extends Component {
    static options = {
      ...Scene.options,
    }

    componentDidMount() {
      this.instance = this.refs.child.getWrappedInstance();
    }

    resendEvent = (eventName, params) => {
      if (this.instance && this.instance[eventName]) {
        this.instance[eventName](params);
      }
    }

    componentDidAppear() {
      this.resendEvent('componentDidAppear');
    }

    componentDidDisappear() {
      this.resendEvent('componentDidDisappear');
    }

    onNavigationButtonPressed(buttonId) {
      this.resendEvent('onNavigationButtonPressed', buttonId);
    }

    render() {
      return (
        <Provider store={store}>
          <Scene ref="child" {...this.props} />
        </Provider>
      );
    }
  };
}

As @Miras4207 said, you should connect your components like this:

export  default connect(mapStateToProps, null, null, {"withRef" : true})(LoginScreen);

I understand the issue better now. So we are having a problem with how react works, it's a problem with supporting any HOC. The problem is we added useful and easy to use API (static options, additional lifecycle methods) but that comes at the price of having to delegate those methods from the HOC, because that's the one being registered. We have no access to the underlying "screen" component.

This is not easy to fix, at least not extensibly (the example above by @yusufyildirim is a good solution but it already breaks because we added another method onSearchBarUpdated).

Reopening the issue until we figure out the best solution. This is not a redux issue, it's supporting any HOC issue.

Please has this issue been solved?

Using the above @yusufyildirim 's HOC and connect I appear to be implementing this wrong, I get Cannot call a class as a function

Here is how I am registering:

import reduxHOC from 'app/routing/reduxHOC';

export function registerScreens() {
    Navigation.registerComponent('Alarm', reduxHOC(ConnectedScreenComponent, store));

@ilovett Hey, registerComponent expects a function as a second argument (which returns a component). You should change your code to this:

Navigation.registerComponent('Alarm', () => reduxHOC(ConnectedScreenComponent, store));

Ah, thank you @yusufyildirim that allowed me to get it running.

@yusufyildirim i get typerror: undefined is not a function (evaluating this.refs.child.getWrappedInstance().
This is my reduxHoc.
`import React, { Component } from 'react';
import { Provider } from 'react-redux';

export default function reduxHOC(Scene, store) {
return class extends Component {
static options = {
...Scene.options,
}

componentDidMount() {
  this.instance = this.refs.child.getWrappedInstance();
}

resendEvent = (eventName, params) => {
  if (this.instance && this.instance[eventName]) {
    this.instance[eventName](params);
  }
}

componentDidAppear() {
  this.resendEvent('componentDidAppear');
}

componentDidDisappear() {
  this.resendEvent('componentDidDisappear');
}

onNavigationButtonPressed(buttonId) {
  this.resendEvent('onNavigationButtonPressed', buttonId);
}

render() {
  return (
    <Provider store={store}>
      <Scene ref="child" {...this.props} />
    </Provider>
  );
}

};
}`

This is the way i register my screen
`import { Navigation } from "react-native-navigation";
import { Provider } from 'react-redux';
import reduxHoc from '../hoc/reduxHoc';
import MyDoorScreen from './MyDoorScreen';
import AdsScreen from './AdsScreen';
import FeedsScreen from './FeedsScreen';
import LoginScreen from './LoginScreen';
import store from '../store';

export default registerScreen = (store, Provider) => {
// registerContainerWithRedux('MyDoorScreen', () => MyDoorScreen, store, Provider);
// registerContainerWithRedux('AdsScreen', () => AdsScreen, store, Provider);
// registerContainerWithRedux('FeedsScreen', () => FeedsScreen, store, Provider);
// registerContainerWithRedux('LoginScreen', () => LoginScreen, store, Provider);
Navigation.registerComponent('AdsScreen', () => reduxHoc(AdsScreen, store));
Navigation.registerComponent('MyDoorScreen', () => reduxHoc(MyDoorScreen, store));
Navigation.registerComponent('FeedsScreen', () => reduxHoc(FeedsScreen, store));
Navigation.registerComponent('LoginScreen', () => reduxHoc(LoginScreen, store));
}`

This is the where i connect my screen to react-redux
`import React, { Component } from 'react';
import { View, Text, NetInfo } from 'react-native';
import { connect } from 'react-redux';
import { fetchAds } from '../actions/AdsAction';
import { Navigation } from 'react-native-navigation';
import SplashScreen from 'react-native-splash-screen'

class AdsScreen extends Component {
constructor(props) {
super(props)

    Navigation.mergeOptions(this.props.componentId, {
        topBar: {
            title : {
                text: 'Trending Now',
                color: '#701112'
            }
        }
    })
}

componentDidMount() {
    SplashScreen.hide();

}

render() {
    console.log(this.props.fetchAds)
    return (
        <View style={styles.containerStyle}>
            <Text> AdsScreen </Text>
        </View>
    )
}

}

const mapStateToProps = state => {
return {

}

}

const styles = {
containerStyle: {
flex: 1,
backgroundColor: '#fff'
}
}

export default connect(mapStateToProps, null, null, {"withRef": true})(AdsScreen);
`
Thanks

@traxx10 I think the HOC looks correct, and also the registerComponent looks correct too. But your mapStateToProps did not have fetchAds. Could you please reformat the code so people can better assist.

Also it will helps if you post react-native, and react version.

Using both @yusufyildirim and @Miras4207 HOC's I'm getting problems with the Scene/comp options. Is there any special way that you need to instantiate the Screen component? Anyone have a fully working example out there for me to reference from?

@yusufyildirim solution only works if you are connecting to a single layer hoc. But the normal use case is, that multiple hocs are piped on top of each other. That way you are loosing access to the instance calls. I don't think the entire interface is feasible for production like this. Is there a discussion on this on any channel that can be joined?

I added support for this here (to be merged soon) but unfortunately at the cost of not being able to automatically have the lifecycle events triggered in the components.

This is a breaking change. You now basically have to manually register for all events, even didApppear and disappear:
const subscription = Navigation.events().registerComponentLifecycleListener(...)
If the listener is your component (doesn't have to be), then don't forget to call subscription.remove() on unmount to avoid leaks.

This will allow nesting any amount of HOCs. If anyone have better suggestions I would appreciate the feedback.

@DanielZlotin, thanks for doing this, Is this mostly for second layer of redux connect like:

Hoc1(Hoc2(connect(Login)))

Can you point us to sample code where we have to manually register all events.

I haven’t ran into this problem but it seems like this might help

https://github.com/mridgway/hoist-non-react-statics

@DanielZlotin with new version I have error when using solution from this thread https://github.com/wix/react-native-navigation/issues/1642#issuecomment-396899419.

image

I removed all componentDidAppear from my screen. And still have this error.

@rendomnet Did you compile your native code after updating?

@yusufyildirim. Why should we use "resendEvent" while react lifecycle still working? Thank.

@DanielZlotin
Documentation is the best in the world!

Can you point us to sample code where we have to manually register all events.

any luck to get wrapper static options working ?

After going through Integration tests for redux in the source code, this is what I came up and works well

reduxStoreWrapper.js :

import React, {Component} from "react";
import {Provider} from "react-redux";

export default reduxStoreWrapper= (ComponentToWrap, store) => {
    return () => {
        return class StoreWrapper extends Component{
            render(){
                return (
                    <Provider store ={store}>
                        <ComponentToWrap {...this.props}/>
                    </Provider>
                );
            }
        }
    }
}

Here {..this.props} is necessary for having componentId in props for every navigation screen which is required while pushing or poping screens in v2.

And while registering screen
screens.js :
Navigation.registerComponent('MyApp.App', ReduxStoreWrapper(App, Store));

What about the performance having a provider created every time we navigate trough screens, also, every screen gets its own provider ... does it impact the performance ?

The people have spoken... #3675

Usage example:

import {Navigation} from 'react-native-navigation';
import {Provider} from 'react-redux';
import {store} from './store';

import PostsList from './posts/screens/PostsList';

export function registerScreens() {
  Navigation.registerComponentWithRedux('blog.PostsList', () => PostsList, Provider, store);
}

Here is an example of a super simple project with Redux
https://github.com/drorbiran/react-native-simple-blog/blob/redux/src/screens.js

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zhanguangao picture zhanguangao  Â·  3Comments

yayanartha picture yayanartha  Â·  3Comments

zagoa picture zagoa  Â·  3Comments

switchtrue picture switchtrue  Â·  3Comments

EliSadaka picture EliSadaka  Â·  3Comments