-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Description
Hi,
This is a more a question than an issue, and I've searched but haven't found anything related to this. Effectively, what I'd like to be able to do is to navigate to a view, and return a result from that view as I dismiss it. From this point I'll call this a ResultNavigator. I believe this is similar to this feature of Native Navigation: https://github.com/airbnb/native-navigation/blob/master/docs/api/navigator/present.md.
A ResultNavigator is typically useful when the user navigates to pick something, or make a choice:
- selecting a user to send a message to from a list of users.
- confirming an action.
I'll give you that there are ways to solve this using Redux for instance, though I do not necessarily think that Redux is the right approach here. I could have a place in my store which contains the action chosen by the user, or a list of users to act on, but I don't find that clean. Especially when the ResultNavigator is meant to be re-used in many different locations for other means (typical for a screen to pick a user), it really has nothing to do with the state of my app, what I'm looking at is just to receive a result.
To be honest, I have succeeded in making this work (I'll leave an example below), but the way I've done it is so ugly that I'd like to do it properly. I am certain there are ways to achieve this with a dedicated Router and Navigator, but as I'm new to this library I could do with some guidance.
Here are my requirements:
- The ResultNavigator is likely to need to be above all other views.
- The Android back button should dismiss and return a, for instance, cancelled result.
- The ResultNavigator could contain its own navigation (multi-step process).
- We can cancel or return a result from any step of the ResultNavigator.
I thought of using a new NavigationAction (dismiss) to indicate to the ResultNavigator that it should discard itself, and I could use the callback _onNavigationStateChange to respond to the pending promises. But it doesn't seem that I can define new NavigationActions, and without that, I'm not certain how to detect when the ResultNavigator should be dismissed or was given a result. I could use props to pass around callbacks, but as the ResultNavigator would likely be a wrapper around another Navigator, I don't think this would work nicely...
Any suggestion is welcome!
Thanks!
Fred
The workaround:
I use the navigation state to pass around callbacks which I inject as properties into the screens. I also inject the property dismissMe which is used when the screen is discarded (back button), or when a thing was chosen.
Note: this was also implemented in such a way that my components ignore everything about the navigation library.
// Navigators.
const ChooseSomething = StackNavigator({
SimpleChoose: {
screen: (props) => {
const goBack = () => props.navigation.dispatch(NavigationActions.back());
return (
<SimpleChooser
onThingChosen={props.navigation.state.params.onThingChosen}
onCancel={props.navigation.state.params.onCancel}
dismissMe={() => goBack()}
goToAdvanced={() => {
return new Promise((resolve, reject) => {
props.navigation.navigate('AdvanceChoose', {
dismissMe: () => {
goBack();
},
onThingChosen: thing => {
resolve(thing);
},
onCancel: () => {
reject()
}
})
});
}}
/>
);
}
},
AdvanceChoose: {
screen: (props) => {
return (
<AdvancedChooser
onThingChosen={props.navigation.state.params.onThingChosen}
onCancel={props.navigation.state.params.onCancel}
dismissMe={props.navigation.state.params.dismissMe}
/>
);
}
},
});
function createRootNavigator(initialRoute = 'NotLoggedIn'){
return StackNavigator({
NotLoggedIn: {
screen: Welcome,
},
LoggedIn: {
screen: (props) => {
return <Home chooseSomething={() => {
return new Promise((resolve, reject) => {
props.navigation.navigate('ChooseSomething', {
onThingChosen: thing => {
resolve(thing);
},
onCancel: () => {
reject();
}
});
});
}} />
},
},
ChooseSomething: {
screen: ChooseSomething
},
}, {
headerMode: 'none',
mode: 'modal',
initialRouteName: initialRoute
});
}
// Components.
class Home extends Component {
pickAthing = (day) => {
this.props.chooseSomething().then(thing => {
doSomethingWithTheThing(thing);
}).catch(() => {
console.log('Pick cancelled');
});
}
}
// Chooser, screen 1.
class SimpleChooser extends Component {
responded = false;
componentDidMount() {
this.responded = false;
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton);
if (!this.responded) {
// The window is being dismissed, make sure we responded.
this.cancel();
}
}
handleBackButton = () => {
this.props.onCancel();
this.responded = true;
this.props.dismissMe();
return true;
}
onChooseHere(thing) {
this.props.onThingChosen(thing);
this.responded = true;
this.props.dismissMe();
}
onClickOnSomething() {
this.props.goToAdvanced().then((thing) => {
this.props.onThingChosen(thing);
this.responded = true;
this.props.dismissMe()
})
}
}
// Chooser, screen 2, optional.
class AdvancedChooser extends Component {
onClickOnSomething() {
const thing = 'aThing';
this.props.onThingChosen(thing);
this.props.dismissMe();
}
}