So, I made YARR (Yet Another React Router)

In a world with many routing solutions for react-native projects, why would I need to custom roll my own? Why couldn’t I just use one of the options discussed in this reddit thread?

Well, I tried. I really did, and none of them fit in with the way I had set up my project or provided the smooth animation performance I was looking for. The reason I needed a flux implementation of routing is that I wanted to be able to route to various screens when users pressed on buttons in modal dialogs.

First, I tried to integrate my existing react-native navigation system with redux. My thinking was that I would dispatch actions to change a state item in my redux store which would then tell react-navigation where to “go”. Unfortunately, I didn’t grok redux integration with react-navigation; likely because of the way I had connected my project and redux. My modals were actual react-native modal components and didn’t have access to react-navigation’s navigator.

Additionally, I tried react-native-router-flux which displayed very slow page transitions once I had it implemented (this could have been with running the project via expo.io, I’m not sure).

In the end I decided to write my own router. I determined, as programmers tend to do, that I could build what I needed more easily than integrating another solution. And yes, I know that the existing solutions contain months if not years of lessons learned, and are more bulletproof, etc.

But, I’m not going to let that stop me…

The basic concept is to conditionally render a component based on a value maintained in my redux store. The selected component is rendered by the App component. It is the highest level component in my application and acts as a frame to contain the applications various screens (pages, scenes, pick whatever metaphor you please) as they are swapped in and out in response to user actions. I also need a stack to push screens (actually just strings of screen names) onto as I navigated to them to enable backwards navigation. I added the following object to my redux store to hold this information:

navigation:{
currentScreen: "Home",
screenStack: ["Home"]

…as you can see, I’m not creative with naming my variables.

The App component looks like this:

<Provider store={store}>
<View style={StyleSheet.absoluteFill}>
{screens[this.props.appState.navigation.currentScreen].screen
</View>
</Provider>

The presence of provider shows that it’s the top level component. I used standard redux techniques to map the application store to the App component’s props.

The next step is to create a structure containing my components. I ended up constructing an object containing the components that made up each screen in my app with the component’s name as the key:

const screens = {
Home: { screen: <Home/>, default: true },
Story: { screen: <Story/> },About: { screen: <About/> },
...
}

Looking back at the App component, it can be seen that if the currentScreen prop in my redux store equals“Home” then {screens[this.props.appState.navigation.currentScreen].screen} returns the Home component. This results in the Home component being displayed inside the App “frame” component.

Next, I created redux actions to move “forward” to new screens, or back to previous screens in the stack. For example:

this.props.dispatch(actions.appStateActions.navigateTo('About'));

dispatches the following action:

navigateTo: (screen)=>{
return{
type: 'NAVIGATE_TO',
screen
}
},

which in turn calls this reducer which changes the currentScreen variable in the application’s redux store to “About”:

case 'NAVIGATE_TO':
return Object.assign({}, appState, { navigation:
Object.assign({}, appState.navigation, {
currentScreen: action.screen,
screenStack:
appState.navigation.screenStack.concat(action.screen)
})
});

No, I don’t currently use immutable.js; yes, I know I should try it some time. I’ll get around to it some day.

Navigating back works similarly, except the reducer looks like this:

case 'NAVIGATE_BACK':
if(appState.navigation.screenStack.length > 1){ // make sure there is something in the stack to go back towards
  // get the screen to go back to
let backTarget = appState.navigation.screenStack.slice(appState.navigation.screenStack.length - 2)[0]; // get the string at index 0 since I want the string, not the array the string is in
  // get the remaining screens so that they can become the updated
    stack array let correctArray = appState.navigation.screenStack.slice(0, - 1);

return Object.assign({}, appState, { navigation: Object.assign({}, appState.navigation, { currentScreen: backTarget, screenStack: correctArray })
});
}
//just return the current appState if there is nothing to go back to return appState;

Finally, Android devices have a back button that requires navigating to the previous screen when pressed. I used BackAndroid to capture back button presses and call the navigationBack() action. I created the listener function in the componentWillMount() method of the App component.

BackAndroid.addEventListener('hardwareBackPress', function () {
if (that.props.appState.navigation.currentScreen !== 'Home') {
that.props.dispatch(actions.appStateActions.navigateBack());
return true;
}
return false;
});

The listener’s callback should return true if the event is handled by the program, and false if it isn’t. In this case, the program only need to navigate back if the user is not on the Homescreen. Otherwise, the callback returns false, indicating that the user is on the Home screen and that Android should exit the program.

That’s it! The result of all this is a system that allows me to navigate anywhere in my app consistently.

My next article will cover how to animate the screen transitions and create a header bar that will be displayed at the top of each screen.


Reginald Johnson has maintained his passion for coding throughout his 20+ year career as an Officer in the United States Navy. He enjoys applying his training and experience in programming, Systems Engineering, and Operational Planning towards programming.