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

How to pass `initialProps` to a StackNavigator screen? #876

Closed
ferologics opened this Issue Mar 31, 2017 · 21 comments

Comments

Projects
None yet
9 participants
@ferologics
Copy link

ferologics commented Mar 31, 2017

Desired behaviour

I'm using one component to display different content based on the initialProps. Think list with 2 data sources that depend on initialProps.

I have a RCTRootView. I modify it's appProperties. Docs say that the component re-renders with new appProperties in this.props on change.

init(bridge: RCTBridge) {
    // init root view without initial props
    self.rootView = RCTRootView.init(bridge: bridge, moduleName: "Foo", initialProperties: nil)
}

// modify app properties later
self.rootView.appProperties = ["listType" : "A"]

The console prints

Running application Foo ({
    initialProps =     {
        listType = A;
    };
    rootTag = 1;
})

But the screen doesn't receive any properties and the component doesn't re-render. I put a breakpoint inside componentWillUpdate to check. Any idea where the props are going and how to access them? They aren't being passed inside navigation state either.

Workarounds

Lazy loaded component

I recall that there was a lazy way to load components getScreen: () => ReactComponent. It might also be too slow to lazy load the component, ideally it's pre-loaded b/c React Native can take some time to render.

const listType
const subscription = notifierEmitter.addListener(eventName, (notification) => {
  listType = notification.listType // <- before navigating, native notifies JS about the list type
})

const Foo = StackNavigator({
  List: { 
    getScreen: () => () => <List listType = { listType }/>,
    path: ':demoListType',
  },
})

This works, but:

  1. The performance is wack
  2. Works ~90% of the time - it's unreliable
  3. The list is jumpy
  4. You can sometimes see previous list type being rendered before the new one appears

I'd like the appProperties be passed to props in the screen.
Better would be if they're passed with navigation as state so that they can be used in navigationOptions for header titles, deep linking, etc.

@ferologics

This comment has been minimized.

Copy link
Author

ferologics commented Apr 3, 2017

@satya164 @grabbou can you tell me where are the appProperties being passed? Thank you.

@ferologics

This comment has been minimized.

Copy link
Author

ferologics commented Apr 3, 2017

They are being passed to NavigationContainer (constructor)

reactnavigaton_initialprops

which passes them inside render()

reactnavigation_initialprops_render

to createNavigator render() View

reactnavigation_initialprops_createnavigator

Then they're passed to CardStack at which point the values are in this.props

reactnavigation_initialprops_viewwillmount

But the render() only receives the default NavigationTransitionProps and in the process of mapping the props to scenes only these default props are passed down.

For now I can fork the implementation and add just add the specific variable I want to be passed, however it'd be great to figure out how to handle the initialProps instead of having them mushed together with the rest of the navigation props.

PS: looking through the source was actually nice, lit code.

@ferologics

This comment has been minimized.

Copy link
Author

ferologics commented Apr 3, 2017

Ok, got a temporary hack in my fork. I check for the route of the component that I want to pass the prop to, and modify the navigation state params.

It would be nice to have this supported by default so I'll try thinking of a way to PR this. In any case, I'd love to hear your ideas on how this could be added.

@ferologics

This comment has been minimized.

Copy link
Author

ferologics commented Apr 3, 2017

So, the component won't re-render on appProperties change b/c react-navigation doesn't refresh state on AppRegistry runApplication call with new initialProps.

After all this I'm forced to use duplicate AppRegistry with same components but different names. Same for UIViewControllers and the RCTRootViews that are instantiating the modules. It's not the worst but it could've been cleaner.

const Foo = StackNavigator({
  List: { 
    screen: List,
    path: ':demoListType',
  }, // ... paths
})

AppRegistry.registerComponent('A', () => Foo);
AppRegistry.registerComponent('B', () => Foo);
@ericvicenti

This comment has been minimized.

Copy link
Contributor

ericvicenti commented Apr 3, 2017

It sounds like you've found a workaround.

To my understanding, this is more of a RN bug than a problem with the nav library. RN makes it easy to provide initial props from native to the root react component, but it is not easy to update the props, so you'd need a better abstraction for that.

@ericvicenti ericvicenti closed this Apr 3, 2017

@ferologics

This comment has been minimized.

Copy link
Author

ferologics commented Apr 3, 2017

@ericvicenti What? the problem is clearly with the library - I still have to utilize a complete wack hack. Please re-open.

The library is messing with basic React Native functionality; my workaround is a total hack of something that should be supported by default. Also the navigation state should be kept up to date with latest app initialProps.

I need not a better abstraction for updating the props, please read the very first issue comment one more time.

Once again, lib issues:

  1. The app's initial props are not passed to the topmost stack component (initial route)
  2. The navigation state is not updated on App restart (ie. on modification of the appProps)
@ferologics

This comment has been minimized.

Copy link
Author

ferologics commented Apr 4, 2017

1 similar comment
@ferologics

This comment has been minimized.

Copy link
Author

ferologics commented Apr 10, 2017

@ericvicenti

This comment has been minimized.

Copy link
Contributor

ericvicenti commented Apr 10, 2017

Hi, I'm not sure why you're calling AppRegistry.registerComponent twice in your app, and that is what confused me.

Have you tried using the screenProps feature? It is documented here: https://reactnavigation.org/docs/navigators/stack#Navigator-Props

const Foo = StackNavigator({
  List: { 
    screen: List,
    path: ':demoListType',
  }, // ... paths
});

const List = ({ screenProps }) => <MyListView foo={screenProps.bar} />;

// first, you can render:
<Foo screenProps={{ bar: true }} />;

// then you can re-render the navigator and the new screen prop will flow down to MyListView:
<Foo screenProps={{ bar: false }} />;
@ferologics

This comment has been minimized.

Copy link
Author

ferologics commented Apr 13, 2017

@ericvicenti calling AppRegistry.registerComponent twice is used for two different RCTRootView inits which have different initialProps. Docs

screenProps doesn't solve anything. The props I need are being passed to the root component from native, not JS.

app registered -> init root view module with initialProps: ["key":"value"] initialized -> props get lost inside the StackNavigator as already described above -> initial screen component can't access initial props

@601367322

This comment has been minimized.

Copy link

601367322 commented Apr 23, 2017

use screenProps
this.props. screenProps

initialProperties:@{@"screenProps":@{@"xxx":@"xxxx"}}

@grabbou

This comment has been minimized.

Copy link
Collaborator

grabbou commented Apr 23, 2017

As already mentioned by others, the way you are doing it:

const Foo = StackNavigator({
  List: { 
    getScreen: () => () => <List listType = { listType }/>,
    path: ':demoListType',
  },
})

is not going to work.

You should instead:

const Foo = StackNavigator({
  List: { 
    screen: List,
    path: ':demoListType',
  },
});

class First extends React.Component {
   render() {
     // read from `initial props`
     return <Foo screenProps={{ listType: this.props... }} />
  }
}

AppRegistry.registerComponent('FirstComponent', () => First);
@joshuapinter

This comment has been minimized.

Copy link

joshuapinter commented May 21, 2017

@grabbou @ferologics I still can't access the initialProps with how you have suggested setting up the app.

I'm not sure if we're doing something wrong. But you have closed this and the other issue that I made regarding the same thing (#634). You seem to be really confident that this is doable so please let us know what your secret here to accessing initialProps using this navigation library.

Thanks!

@joshuapinter

This comment has been minimized.

Copy link

joshuapinter commented May 21, 2017

@grabbou @ferologics
And if you could please re-open this issue until one of us has confirmed that it's been resolved, that would be great. Thanks.

@joshuapinter

This comment has been minimized.

Copy link

joshuapinter commented May 21, 2017

Okay, finally understood the approach you were taking and managed to get it working. To help others, here is a full solution:

import React from 'react';

import {
  AppRegistry,
  Text,
} from 'react-native';

import { StackNavigator } from 'react-navigation';

class MyApp extends React.Component {
  render() {
    console.log('this.props in MyApp', this.props); // This will list the initialProps.

    // StackNavigator **only** accepts a screenProps prop so we're passing
    // initialProps through that.
    return <Navigator screenProps={this.props} />; 
  }
}

class HomeScreen extends React.Component {
  render() {
    console.log('this.props in HomeScreen', this.props);
    // This will output something like this: 
    // { screenProps: { ...your initialProps }, navigation: { ...StackNavigator stuff... } }

    return <Text>My initialProps are {JSON.stringify(this.props.screenProps)}.</Text>;
  }
}

const Navigator = StackNavigator({
  Home: { screen: HomeScreen },
});

AppRegistry.registerComponent('MyApp', () => MyApp);

Thanks and I hope that helps clarify for others, including @ferologics.

@ferologics

This comment has been minimized.

Copy link
Author

ferologics commented May 24, 2017

Hey, after this issue, I decided that React Navigation is performing extremely poorly in my production app and I decided to go with native iOS navigation + a bunch of RCTRootViews.

((( why I decided to do so:

  • performance problems were with the navigation animations (pop, push were laggy)
  • popping the entire screen stack resulted in an unexpected animation which caused confusion to many users.
  • #729

After testing with couple users they all concurred that the navigation is unlike the native one. Overall less responsive UI and there were subtle abovementioned animation differences that influenced the UX. )))

@grabbou and @ericvicenti are totally right, I should've used the screenProps as suggested in above examples.

My error was in expecting the StackNavigator to take the initialProps and automagically pass them to the initial screen, without requiring an intermediary component to make use of the screenProps.

^ Feedback to consider for the project collaborators and a lesson for me to read docs more thoroughly.

Thanks for all your support!

@robertsimoes

This comment has been minimized.

Copy link

robertsimoes commented May 31, 2017

@joshuapinter I ❤️ U.

@joshuapinter

This comment has been minimized.

Copy link

joshuapinter commented May 31, 2017

@LouisJS

This comment has been minimized.

Copy link

LouisJS commented Nov 30, 2017

Works perfectly ! Thanks @joshuapinter

@puckybreg

This comment has been minimized.

Copy link

puckybreg commented Mar 12, 2018

@ferologics what debugger are you using for react-native?

@silhouette5366

This comment has been minimized.

Copy link

silhouette5366 commented Feb 13, 2019

@joshuapinter @grabbou can you guys please give a solution which works for react-navigation-3 in which you must set up your app container directly.

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