In iOS, we can change title and buttons of a navigation bar from the view controller, e.g. via the navigationItem property. This is useful, for example, to let the view controller itself handle the right/left buttons' event.
In React Native, the Navigator's navigationBar is rendered in the container (where <Navigator> is placed) and decoupled from the underlying scene. Buttons event handlers must be defined in the container, where we can only have access to a scene with the ref prop. More scenes there are, more the container becomes complicated. It get even more complex when the buttons handlers depend from the scene's state.
I'd prefer instead to define the navigation bar's buttons (and title) inside the scene itself, e.g. by rendering Navigator.NavigationBar as child of the scene component – but I couldn't get it working.
I wonder then what is the best approach: am I missing the sense of NavigationBar, since it seems designed just to pop/push routes? As alternative I could adopt a special flux store to help the communication between scene and the NavigatorBar's routeMapper, but it seems overly complicated for a common UI element like the Navigator.
For example, here's a <LoginForm> with a submit() function, and an action button to start the login process from the navigation bar:

My code now looks like:
class Application extends Component {
render() {
const navigationBar = (
<Navigator.NavigationBar routeMapper={{
RightButton(route, navigator) {
return (
<NavButton
text="Login"
onPress={ () => this.refs.loginScene.submit() }
/>
);
}
}} />
)
return (
<Navigator
renderScene={ () => <LoginForm ref="loginScene" /> }
navigationBar={ navigationBar }
/>
);
}
}
cc @ericvicenti @hedgerwang
It would be difficult to put contents of the nav bar within the scene, as they may have different lifespans.
You could try this, where the route owns the button press events:
class LoginRoute {
constructor() {
this.eventEmitter = new EventEmitter();
}
renderRightButton(navigator) {
return (
<NavButton
text="Login"
onPress={ () => this.eventEmitter.emit('loginPress') }
/>
);
}
renderScene(navigator) {
return (
<LoginScene
routeEvents={this.eventEmitter}
navigator={navigator}
/>
);
}
}
class Application extends Component {
static navBarRouteMapper = {
RightButton(route, navigator) {
return route.renderRightButton(navigator);
}
}
render() {
return (
<Navigator
initialRoute={ new LoginRoute() }
renderScene={ (route, navigator) => route.renderScene(navigator) }
navigationBar={
<Navigator.NavigationBar
routeMapper={Application.navBarRouteMapper}
/>
}
/>
);
}
}
Thanks @ericvicenti for the idea, this pattern works much better to split the routes from the navigator container 👍
However, it doesn't help so much to control the navigation bar according to the scene state. Take as example the iOS keyboard's settings: it seems to update the scene state (applying an animation), while changing the navigation bar as well:

The workaround I'd follow is to push the same route with an attribute which say the scene to "animate" its content once it is mounted. The navigation bar animation doesn't play as good as the native one, but I guess I can try to provide a custom one in the configureScene.
I agree that it looks like the state is stored in the scene, but it would actually be stored in the route because the scene is an isolated component.
You're right that the Navigator.NavigationBar isn't nearly as complete as the native one. We could extend it with the Animated API and start giving it more of these features.
@gpbl: I handle this by defining a generic navigationBarRouteMapper that takes attributes out of the route to render the navigation bar. Then, each scene is able to customize its navigation bar in componentWillMount(), which gets called (thankfully) before the navigationBarRouteMapper does its work.
So, for instance, I have a scene that configures its navigation bar like this (in componentWillMount()):
this.props.route.title = "Scene Title";
this.props.route.leftButtonText = "Cancel";
this.props.route.onPressLeftButton = function() {
this.props.navigator.pop();
}.bind(this);
this.props.route.rightButtonText = "Save";
this.props.route.onPressRightButton = this.save.bind(this);
Then, the functions in the navigationBarRouteMapper look for these attributes in the route. For instance, here's Title():
Title: function(route) {
return (
<Text style={[styles.navBarText, styles.navBarTitleText]}>
{route.title}
</Text>
);
}
Admittedly, it goes against the grain of React's top-down flow of data: I'm passing data up to the navigationBarRouteMapper through the route. Also, it only works because componentWillMount() gets called before the navigationBarRouteMapper functions.
But, on the flip side, things work mostly as they did in ObjC, when we could manipulate self.navigationController.navigationItem in viewDidLoad(). (I'm currently looking for a way to hide the navigation bar on a per-scene basis, which I don't seem to be able to do at the moment.)
Would be very interested in any feedback on this approach.
if it helps, gb-native-router lets you talk to your navBar from the scene using this.props.setRightProps and this.props.setLeftProps. You can check out index.js to see how it's done if you'd like.
Would be very interested in any feedback on this approach.
@jedlinlau This is interesting, but I really don't want to mutate this.props :smile: Also props are being frozen starting react 0.14
We could extend it with the Animated API and start giving it more of these features.
@ericvicenti Is this something you're waiting for the community to pick up? I've seen a Navigator example in the Animated documentation. I'd be interested in contributing here, to get better animations and to figure out the communication problem between the nav bar and the scene. I do like the route approach!
@ehd: Makes sense. I like @MikaelCarpenter 's suggestion of passing the required callbacks down as props.
Coming in way late on this, but we use events for communicating between the navbar and scene, simple and effective.
If you want to go a little further than events, I encourage you to use a flux architecture, you'll then just have to trigger the correct action in your Navigator or in your content View :smiley:
This is how I solved it, though I'm not really happy with it :confused:
import NavigationBar from 'react-native-navbar'
<Navigator
initialRoute={{
Component: InitialComponent,
navigationBarProps: {
title: 'First'
}
}}
renderScene={(route, navigator) => {
const {
Component,
passProps,
navigationBarProps
} = route
if (!route.NavigationBar) {
route.NavigationBar = NavigationBar
}
const props = {
...this.props,
...passProps,
// XXX: this does not feel right oO
setNavigationBarProps: props => {
route.navigationBarProps = {
...route.navigationBarProps,
...props
}
setTimeout(() => this.forceUpdate(), 0)
}
}
return (
<View style={{flex: 1}}>
<route.NavigationBar
{...navigationBarProps}
navigator={navigator}
router={route}
/>
<Component {...props} navigator={navigator} />
</View>
InitialComponent:
onRenderSecond = () => {
this.props.navigator.push({
Component: Second,
navigationBarProps: {
title: 'Will be overridden by the component'
}
})
}
Second:
componentWillMount () {
this.props.setNavigationBarProps({
title: 'Second',
nextTitle 'Save',
onNext: () => {
this.props.onSave(this.state)
this.props.navigator.pop()
}
})
All I want to do is hide the NavigationBar for various components, e.g. Page A doesn't need navbar, but Page B does, what's the easiest way to do that? Can someone give me a code example?
@niftylettuce
What I do now is wrapping the NavigatorNavigationBar:
import React, {
Navigator,
PropTypes,
Component
} from 'react-native'
export default class ExNavigationBar extends Component {
static propTypes = {
navState: PropTypes.object.isRequired,
navigationBarHidden: PropTypes.bool
}
// this is important, if this is omitted, the navbar will render the old route again, not the new one
updateProgress (...args) {
this.state.navigationBar && this.state.navigationBar.updateProgress(...args)
}
setNavigationBarRef = navigationBar => {
this.setState({
navigationBar
})
}
render () {
if (this.props.navState.routeStack.slice(-1)[0].navigationBarHidden === true) {
return null
} else {
return (
<Navigator.NavigationBar
ref={this.setNavigationBarRef}
{...this.props}
/>
)
}
}
}
That I pass to ExNavigator:
<ExNavigator
{...this.props}
renderNavigationBar={props => <ExNavigationBar {...props}/> }
configureScene={route => Navigator.SceneConfigs.FloatFromBottom}
initialRoute={{
// this is for hiding the navbar
navigationBarHidden: true,
getSceneClass() {
return require('./HomeScreen');
},
getTitle() {
return 'Home'
},
}}
/>
In HomeScreen you can then push a route that does not hide the navbar:
// ...
nextScreen = () => {
this.navigator.push({
// could be omitted, since it defaults to false
navigationBarHidden: false,
getSceneClass() {
return require('./NextSreen')
},
getTitle() {
return 'Next Screen'
}
})
}
// ...
This is for illustration purposes, use a factory for the route creation. For more information about ExNavigator see the medium post.
If anything is unclear, please feel free to ask. I also wrote a Route class that makes it easier to create routes, defer rendering (like ExNavigator's _LoadingContainer_) and automatically hooks up an event emitter (and also disposes it) so the rendered scene can change the navbar's title, buttons, etc., but at the moment it's still integrated in out app and I haven't had the time to clean it up and put it into a module. But if there is enough interest I might do that next weekend.
What do you guys think about passing down RxObservable(with some action related with Navigation Bar) for the underlying component to subscribe to that observable?
var PlainNavigator = React.createClass({
...
_navBarRouter: {
Title: (route, navigator, index, navState) => {
...
},
LeftButton: (route, navigator, index, navState) => {
...
return (<NavigationTextButton
buttonText={routes.getCurrentRoute().leftButtonText}
onPress={() => navigator.props.leftButtonSubject.onNext(routes)} />);
...
},
RightButton: (route, navigator, index, navState) => {
...
return (<NavigationTextButton
buttonText={routes.getCurrentRoute().rightButtonText}
onPress={() => navigator.props.rightButtonSubject.onNext(routes)}/>);
...
}
},
_renderScene: function(route, navigator) {
...
<Screen
//subscribe to these subjects if need to receive left,right button events
leftButtonSubject={this._leftButtonSubject}
rightButtonSubject={this._rightButtonSubject}
routes={routes}
navigator={navigator}
api_domain={this.props.api_domain} />
);
...
},
_leftButtonSubject: new Rx.Subject(),
_rightButtonSubject: new Rx.Subject(),
render: function() {
return (
<Navigator
leftButtonSubject={this._leftButtonSubject}
rightButtonSubject={this._rightButtonSubject}
initialRouteStack={this.getInitialRouteStack(this.props.uri)}
renderScene={this._renderScene}
navigationBar={
<Navigator.NavigationBar
routeMapper={this._navBarRouter}/>
}
/>
);
}
});
Using RxJs, I created RxSubjects so that bottom components of the Navigator can also receive button events by subscribing to the RxSubjects.
I solved this whole thing with a wrapper over RN's Navigator. It is available as an NPM Package here -- https://github.com/rahuljiresal/react-native-rj-navigator
BTW if you are using RN 0.16 you might reach a bug where elements inside navbar are not touchable.
I've fixed it in https://github.com/facebook/react-native/pull/4786
You can check my solution here:
https://github.com/machard/react-native-advanced-navigation
especially the scene binding example is here https://github.com/machard/react-native-advanced-navigation/blob/master/src/containers/BindingExample.js
@gpbl I have a scene that configures its navigation bar like this in componentWillMount()
this.props.route.title = "Scene Title";
Then, the functions in the navigationBarRouteMapper
Title: function(route) {
return (
<Text style={[styles.navBarText, styles.navBarTitleText]}>
{route.title}
</Text>
);
}
but got an error: this.props attempted to assign to readonly property.
RN: 0.20.0
how to fix it?
@machard it's great ! Thank you !
Hey guys,
I've read through, checked other people's solution and wasn't really happy with with any of them. After some thinking I ended up with this:
var RegistrationPage = React.createClass({
render: function() {
return <Text>Done</Text>
},
statics: {
leftButtonMapper: function(route, navigator, index, navState) {
return <NavigationBackButton onPress={() =>
navigator.pop()
} />
},
},
})
And my navigation component contains these:
var AuthProcess = React.createClass({
render: function() {
return (
<Navigator
initialRoute={{page: 'login'}}
renderScene={this.renderScene}
navigationBar={this.navigationBar()}
/>
)
},
renderScene: function(route, navigator) {
// ...
}
navigationBar: function() {
return <Navigator.NavigationBar
routeMapper={{
LeftButton: this.leftButtonMapper,
RightButton: this.rightButtonMapper,
Title: this.titleMapper
}}
/>
},
leftButtonMapper: function(route, navigator, index, navState) {
return this.navigatorItem('leftButtonMapper', route, navigator, index, navState);
},
rightButtonMapper: function(route, navigator, index, navState) {
return this.navigatorItem('rightButtonMapper', route, navigator, index, navState);
},
titleMapper: function(route, navigator, index, navState) {
return this.navigatorItem('titleMapper', route, navigator, index, navState);
},
navigatorItem: function(functionName, route, navigator, index, navState) {
let classDefinition;
classDefinition = this.classForRoute(route);
if(classDefinition && classDefinition[functionName]) {
return classDefinition[functionName](route, navigator, index, navState);
} else {
return null;
}
},
classForRoute: function(route) {
switch(route['page']) {
case 'login':
return LoginPage;
case 'signUpWithEmail':
return RegistrationPage;
default:
return null;
}
}
})
I'm new to RN, so maybe this is breaking some patterns. I'm quite curious for feedback.
Z.
My solution is instead of storing input data in scene component state, and call setSetate in onChangeText callback function, onChangeText calls a function passed from props which updates the route object. This solution even eliminates the local state in child component, which can then be make pure. However, I do notice lag when typing. Below is some part of my code:
// function to be called by onChangeText
const updateAddItemRoute = (navigator, newProp) => {
navigator.replace(Object.assign({
id: 'addItem',
title: 'Add New Sth',
text: '',
}, newProp));
}
// renderScene
case 'addItem':
return (
<AddItem
text={route.text}
nav={nav}
updateAddItemRoute={updateAddItemRoute}
/>
);
// scene component render function
render() {
const { text, updateAddItemRoute, nav } = this.props;
return (
<View style={styles.scene}>
<Text style={styles.inputLabel}>
Name
</Text>
<TextInput
style={styles.inputBox}
onChangeText={(text) => updateAddItemRoute(nav, {text})}
value={text}
autoFocus={true}
placeholder={'Enter Task Name'}
returnKeyType={'done'}
/>
</View>
)
}
Still working on better solution :)
@pallzoltan This is almost perfect. Now i just need to call methods and setState from within the component. My problem is that its a static so i don't have the correct this context.
Does someone know how i can pass the current instances of the scene to the navigationBarMapper methods?
My app.js
static mappedRoutes = (route)=> {
const routeMap = new Map([
[Constants.Routes.CONTACT, Contact],
[Constants.Routes.DASHBOARD, Dashboard],
[Constants.Routes.DISCLAIMER, Disclaimer]
]);
if (routeMap.has(route.name)) {
return routeMap.get(route.name);
} else {
return null;
}
};
getNavigatorItem(functionName, route, navigator, index, navState) {
// i need a way to call the 'leftButton' function from the scene instance and not the static leftButton function. How can i access the current rendered scene instance?
const Route = App.mappedRoutes(route);
if (Route && Route[functionName]) {
return Route[functionName](route, navigator, index, navState);
}
return null;
}
renderScene(route, navigator) {
const Route = App.mappedRoutes(route);
if (Route) {
const selector = Route.selector ? Route.selector : ()=> {
return {}
};
const connectedRoute = connect(selector)(Route);
return React.createElement(connectedRoute, {...route.passProps, navigator: navigator});
}
return <Text>404</Text>;
}
renderNavigationBar() {
if (this.state.navigationBarHidden) {
return <View/>;
}
const navigationBarStyle = {
backgroundColor: this.state.navigationBarColor
};
return (
<Navigator.NavigationBar
routeMapper={this.createRouteMapper()}
style={[Style.navigationBar, navigationBarStyle]}
/>
);
}
mapTitleToRoute(route, navigator, index, navState) {
return (
<TextTitlebar title={route.title ? route.title.toUpperCase() : ''}
textStyle={[Style.titleBarText, route.titleTextStyle]}
containerStyle={[route.titleStyle]}
/>
);
}
mapLeftButtonToRoute(route, navigator, index, navState) {
if (index > 0) {
return <BackButton onPress={this.onPressBackButton}/>;
} else if (index === 0) {
return <MenuButton onPress={this.onPressMenuButton}/>;
}
}
mapRightButtonToRoute(route, navigator, index, navState) {
const navigatorItem = this.getNavigatorItem('rightButton', route, navigator, index, navState);
if (navigatorItem) {
return navigatorItem;
}
switch (route.name) {
case Routes.SHOPPING:
return <MenuButton onPress={this.onPressMenuButton}/>;
default:
return <View/>;
}
}
createRouteMapper() {
return {
LeftButton : this.mapLeftButtonToRoute,
RightButton: this.mapRightButtonToRoute,
Title : this.mapTitleToRoute,
}
}
My dashboard component
class Dashboard extends Component {
constructor(props) {
super(props);
}
render() {
return (
<View style={Style.container}>
</View>
)
}
static leftButton(){
return <Text>LEFT BUTTON</Text>
}
static rightButton(){
// i want here something like this.setState({foo: 'bar'})
return <Text>RIGHT BUTTON</Text>
}
static selector(state){
return {
User: state.User
}
}
}
export default Dashboard;
Closing this out because we aren't changing the API of Navigator any more.
@maluramichael, you could use a flux library to subscribe your header components to changing data and allow your inner scene to communicate with the header.
Most helpful comment
Closing this out because we aren't changing the API of Navigator any more.
@maluramichael, you could use a flux library to subscribe your header components to changing data and allow your inner scene to communicate with the header.