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.
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)
}
}
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
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.
Most helpful comment
I think in general, you should use
Actions.createwhen dynamically changing routes as shown below: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.