React-native-router-flux: Key <keyname> is already defined!

Created on 1 Jun 2016  路  8Comments  路  Source: aksonov/react-native-router-flux

Hello,

I have an onboarding tutorial in my application. I would like to return some Scene if it's the first time or not. Once I create the Router with the scenes, I got a Key <keyname> is already defined!, because it tries to re-render the Router. I would like to know if there is one better way to do that.

To do what I would like to do, I'm using a state which I make sure I will render the Scene part just one time.

  • react-native-router-flux 3.26.5
  • react-native v0.26.0

Before(without a complete state):

render() {
    var showTutorialView, showMainScreen;

    switch (this.state.onboarding) {
      case 'pending':
        showTutorialView = (<Scene key="tutorialView" component={TutorialView} title="Tutorial View" initial />);
        showMainScreen = (<Scene key="mainScreen" component={MainScreen} title="My App" />);
        break;
      case 'done':
        showTutorialView = (<Scene key="tutorialView" component={TutorialView} title="Tutorial View" />);
        showMainScreen = (<Scene key="mainScreen" component={MainScreen} title="My App" initial />);
        break;
    };

      return (<Router>
        <Scene key="root">
          {showTutorialView}
          <Scene key="validateNumberScreen" component={ValidateNumberScreen} title="Validate Number" />
          <Scene key="verifyPinScreen" component={VerifyPinScreen} title="Verify PIN" />
          <Scene key="personalInformationScreen" component={PersonalInformationScreen} title="Personal Information" />
          {showMainScreen}
        </Scene>
      </Router>);
  }

After:

render() {
    var showTutorialView, showMainScreen;

    switch (this.state.onboarding) {
      case 'pending':
        showTutorialView = (<Scene key="tutorialView" component={TutorialView} title="Tutorial View" initial />);
        showMainScreen = (<Scene key="mainScreen" component={MainScreen} title="My App" />);
        break;
      case 'done':
        showTutorialView = (<Scene key="tutorialView" component={TutorialView} title="Tutorial View" />);
        showMainScreen = (<Scene key="mainScreen" component={MainScreen} title="My App" initial />);
        break;
    };

    if(this.state.completed) {
      return (<Router>
        <Scene key="root">
          {showTutorialView}
          <Scene key="validateNumberScreen" component={ValidateNumberScreen} title="Validate Number" />
          <Scene key="verifyPinScreen" component={VerifyPinScreen} title="Verify PIN" />
          <Scene key="personalInformationScreen" component={PersonalInformationScreen} title="Personal Information" />
          {showMainScreen}
        </Scene>
      </Router>);
    }
    else{
      return (null)
    }
  }

Most helpful comment

I think in general, you should use Actions.create when dynamically changing routes as shown below:

import {Actions, Scene, Router} from 'react-native-router-flux';

const scenes = Actions.create(
  <Scene key="root">
    <Scene key="login" component={Login} title="Login"/>
    <Scene key="register" component={Register} title="Register"/>
    <Scene key="home" component={Home}/>
  </Scene>
);

/* ... */

class App extends React.Component {
  render() {
    return <Router scenes={scenes}/>
  }
}

That said, you should not be re-rendering. Your initial only cares about the first render anyways. So rather than use state, just check for the onboarding once on app load.

All 8 comments

I think in general, you should use Actions.create when dynamically changing routes as shown below:

import {Actions, Scene, Router} from 'react-native-router-flux';

const scenes = Actions.create(
  <Scene key="root">
    <Scene key="login" component={Login} title="Login"/>
    <Scene key="register" component={Register} title="Register"/>
    <Scene key="home" component={Home}/>
  </Scene>
);

/* ... */

class App extends React.Component {
  render() {
    return <Router scenes={scenes}/>
  }
}

That said, you should not be re-rendering. Your initial only cares about the first render anyways. So rather than use state, just check for the onboarding once on app load.

@cridenour Thank you !

No more Key is already defined! messages in the debugger !

I have already tried that. But it seems it does not work. I have the state set up on componentDidMout, it calls the render again when the state is changed.

export default class App extends Component {

  state = {
    onboarding: 'pending',
  }

  componentDidMount(){
    AsyncStorage.getItem('onboarding').then((val) => {

      if (!val) {
        this.setState({ onboarding: 'pending'});
        AsyncStorage.setItem('onboarding', 'done').done();
      } else {
        this.setState({ onboarding: val});
      }

    }).done();
  }

  render() {
    var showTutorialView, showMainScreen;

    switch (this.state.onboarding) {
      case 'pending':
        showTutorialView = (<Scene key="tutorialView" component={TutorialView} title="Tutorial View" initial />);
        showMainScreen = (<Scene key="mainScreen" component={MainScreen} title="My App" />);
        break;
      case 'done':
        showTutorialView = (<Scene key="tutorialView" component={TutorialView} title="Tutorial View" />);
        showMainScreen = (<Scene key="mainScreen" component={MainScreen} title="My App" initial />);
        break;
    };

    var scenes = Actions.create(
      <Scene key="root">
        {showTutorialView}
        <Scene key="validateNumberScreen" component={ValidateNumberScreen} title="Validate Number" />
        <Scene key="verifyPinScreen" component={VerifyPinScreen} title="Verify PIN" />
        <Scene key="personalInformationScreen" component={PersonalInformationScreen} title="Personal Information" />
        {showMainScreen}
      </Scene>
    );

    return (<Router scenes={scenes} />);
  }
}

That's because AsyncStorage is a Promise and will call setState once it returns. To truly make it work add a new function like

async function getOnboard() {
  const onboard = await AsyncStorage.getItem('key');
  return onboard;
}

And then in your componentDidMount you can use this.getOnboard.done() to get the value and it will work as you expect.

@cridenour so, if you want to key off of a slice of the state in order to hide or show a nav or tab bar, how do you do that if it's not supposed to be re-rendered?

@chandlervdw When I did that on an app, I forked the TabBar into it's own component which listened to a flux store, and would hide/show appropriately. It's just the Router itself that shouldn't re-render.

@cridenour makes sense 鈥斅爃ow did you reference your forked TabBar component within the Scene?

@chandlervdw Oh right. On your scene with tabs={true} you can pass in component={MyTabBar} and it will use that rather than importing the default.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

llgoer picture llgoer  路  3Comments

moaxaca picture moaxaca  路  3Comments

wootwoot1234 picture wootwoot1234  路  3Comments

kirankalyan5 picture kirankalyan5  路  3Comments

YouYII picture YouYII  路  3Comments