It pays to have the Navigator
at the root of your application. This allows you to tunnel back and render something at the root. In our case a custom Modal overlay component. You can pass anything on the route object, and anytime you render the same component at the same place it will just re-render that same component. So lets use the power of React to solve our problems.
Lets setup our app
var React = require("react-native"); var { AppRegistry, StyleSheet, Text, View, Navigator, TouchableOpacity, Animated, Dimensions, } = React; var { height: deviceHeight } = Dimensions.get("window");
We'll get our deviceHeight
so we can manually animate our modal up. You could use LayoutAnimation
here as well and not deal with getting the deviceHeight but I like Animated
so deal with it.
var RouteStack = { app: { component: App, }, };
This is our super complex route stack. All we do is have a named route with the componet to render.
var ModalApp = React.createClass({ getInitialState: function () { return { modal: false }; }, renderScene: function (route, navigator) { var Component = route.component; return <Component openModal={() => this.setState({ modal: true })} />; }, render: function () { return ( <View style={styles.container}> <Navigator initialRoute={RouteStack.app} renderScene={this.renderScene} /> {this.state.modal ? ( <TopModal closeModal={() => this.setState({ modal: false })} /> ) : null} </View> ); }, });
This is the root of our application. Our render function is pretty basic. We render Navigator
with our intialRoute
being our only Route. Our renderScene
function is going to control our logic.
When we render our component we pass down an openModal
function. This will set modal:true
on our state which will allow for us to open/close the modal over the current route. This will just cause Navigator
to re-render at the current route. This means your rendered Component
at the current route will have componentWillReceiveProps
triggered. Our TopModal
will receive a closeModal
function to set modal:false
on state and unmount our TopModal
.
We put our modal after the Navigator
so we can render on top of it.
var App = React.createClass({ render: function () { return ( <View style={styles.flexCenter}> <TouchableOpacity onPress={this.props.openModal}> <Text>Open Modal</Text> </TouchableOpacity> </View> ); }, });
All we do is when we want to open the modal just call the openModal
function on props. That will call up to the function in Navigator
renderScene
and pop open the modal over the existing app.
var TopModal = React.createClass({ getInitialState: function () { return { offset: new Animated.Value(deviceHeight) }; }, componentDidMount: function () { Animated.timing(this.state.offset, { duration: 100, toValue: 0, }).start(); }, closeModal: function () { Animated.timing(this.state.offset, { duration: 100, toValue: deviceHeight, }).start(this.props.closeModal); }, render: function () { return ( <Animated.View style={[ styles.modal, styles.flexCenter, { transform: [{ translateY: this.state.offset }] }, ]} > <TouchableOpacity onPress={this.closeModal}> <Text style={{ color: "#FFF" }}>Close Menu</Text> </TouchableOpacity> </Animated.View> ); }, });
Here we have a basic modal. We set the translateY
to the full device height so that it renders off screen, and on mount we slide it up in 100ms
. On close we slide it down, call the closeModal
which will trigger the re-render in our renderScene
. This case we won't have modal: true
set so our TopModal
will just unmount.
Hey now go get your modal on. Just remember, React is flexible. Sometimes you need to pass something up to render at the top. Yes slightly a pain, but it's a manageable pain.
var React = require("react-native"); var { AppRegistry, StyleSheet, Text, View, Navigator, TouchableOpacity, Animated, Dimensions, } = React; var { height: deviceHeight } = Dimensions.get("window"); var TopModal = React.createClass({ getInitialState: function () { return { offset: new Animated.Value(deviceHeight) }; }, componentDidMount: function () { Animated.timing(this.state.offset, { duration: 100, toValue: 0, }).start(); }, closeModal: function () { Animated.timing(this.state.offset, { duration: 100, toValue: deviceHeight, }).start(this.props.closeModal); }, render: function () { return ( <Animated.View style={[ styles.modal, styles.flexCenter, { transform: [{ translateY: this.state.offset }] }, ]} > <TouchableOpacity onPress={this.closeModal}> <Text style={{ color: "#FFF" }}>Close Menu</Text> </TouchableOpacity> </Animated.View> ); }, }); var App = React.createClass({ render: function () { return ( <View style={styles.flexCenter}> <TouchableOpacity onPress={this.props.openModal}> <Text>Open Modal</Text> </TouchableOpacity> </View> ); }, }); var RouteStack = { app: { component: App, }, }; var ModalApp = React.createClass({ getInitialState: function () { return { modal: false, }; }, renderScene: function (route, navigator) { var Component = route.component; return <Component openModal={() => this.setState({ modal: true })} />; }, render: function () { return ( <View style={styles.container}> <Navigator initialRoute={RouteStack.app} renderScene={this.renderScene} /> {this.state.modal ? ( <TopModal closeModal={() => this.setState({ modal: false })} /> ) : null} </View> ); }, }); var styles = StyleSheet.create({ container: { flex: 1, }, flexCenter: { flex: 1, justifyContent: "center", alignItems: "center", }, modal: { backgroundColor: "rgba(0,0,0,.8)", position: "absolute", top: 0, right: 0, bottom: 0, left: 0, }, });