DEV Community

loading...

Migrating from react-native-router-flux to react-router for web compatibility

Zia Ul Rehman
Originally published at ziatechblog.wordpress.com on ・8 min read

Part of our effort to compile our react-native app for web, so we can have kind of a PWA built in react-native which runs on iOS, android and app, all 3 with same codebase.

We did this migration keeping the interface as close to react-native-router-flux as we could so there are minimal required changes in the app , and in some cases we were able to just change the import and that was it.

Trying to avoid replacement

We were usingreact-native-router-flux v4.0 in our RN(0.59.9) app, this version is based on react-navigation v2. When compiling for web, we obviously need compatible libraries. Although react-navigation didn’t support web officially in v2, we gave it a try, many things were working, we could navigate just fine. but issue was, there was no url paths being changed while navigating(among some other issues).

We need to show urls to users so users can bookmark, share or do whatever they want with those urls.

Let’s try an upgrade?

react-navigation v3 is officially supporting web now, although in early stages, there have been a lot of work for web support and efforts are still underway, but progress on the web part seems a bit slow(at the time of writing) with lot of important use-ability related issues open with little to no efforts to resolve those issues. But as it was most easy to just try upgrading our existing library’s version, we gave it a try.

react-native-router-flux v4.1(which is in beta, at the time of writing) uses v3 of react-navigation, so we upgraded to that and issues were still there. Issues like:

  • No URL paths, only domain name kept showing on all pages.
  • Pages were not scrollable on web when bottom navigation was in use.

So as odds were heavily against this setup to work, we decided to just change the library and choose an alternative.

Why react-router?

react-router is the most mature library which supports both native and web , although packages for web and native are different but API is almost identical, which is a huge benefit, as it allows us to conditionally import(based on OS) and forget about the rest, as API is same(you will see this implemented in a minute).

It is also a good choice for cross platform compatibility because unlike react-navigation it only concerns itself with navigation, building UI for bottom or top or whatever navigations is not its job, that reduces parts which needs to be fixed for web and makes compatibility a lot easier.

Gotchas

Before we jump into conversion, lets discuss some gotchas:

  1. Depending upon size of your application, this can be a lot of rework, we did worked almost 2 weeks to set our app fully working with this new library.
  2. You can’t pass functions(callback etc) or any other serializable object in state for next route.
  3. You will have to redo any non-routing features you are using from react-navigation (i am referring to react-navigation as react-native-router-flux is based on that and all features it gives us are based on those in react-navigation). What I mean by non-routing features are features like building UI for bottom navigation and anything like that.
  4. To keep the API same, we will use render props pattern to spread initial props/state and even url params to the component being rendered on any specific route.

Actual Conversion

First, add these packages: react-router-dom and react-router-native, make sure their versions match.

Now, we will need to write a couple of special files, which will contain platform specific conditional imports etc. Then, we will use these files to import related components instead of importing from actual libraries. You can consider it kind of wrappers.

We will be taking an incremental approach, so we will not remove the router-flux in start, rather we will keep it until we are finished with our whole process.

Let’s start with magic files(you can copy these as is).

Magic Files

1. Router.js file:

import { Platform } from 'react-native'; 

const isWeb = Platform.OS === 'web'; 

const RouterPackage = isWeb ? require('react-router-dom') : require('react-router-native'); 

export const Router = RouterPackage.Router; 
export default RouterPackage;

_ Disclaimer: This(and some others) code is taken from somewhere on the web(maybe it was a github issue or something)_.

Place this file at the location of your preference(we kept it at root, for now). This file is doing nothing fancy, it is just conditionally importing correct library, when its web, we import from react-router-dom otherwise we import from react-router-native.

2. RouteHistory files:

For this, we needed to create two files, first RouteHistory.js:

import { createMemoryHistory } from "history"; 
export default createMemoryHistory();

And for web another file, RouteHistory.web.js:

import { createBrowserHistory } from "history"; 
export default createBrowserHistory();

If you don’t know what this .web.js extension is, and you are not working on a project which will compile to web, you can skip the web.js file. As, keeping it short, .web.js file is used instead of .js file on the web, when available. Moving on.

What these files are doing is just creating a history object , which we will pass to react-router so a proper history strategy is used as per platform, memoryHistory on mobile and browserHistory on web.

Routes Mapping

Now we have our magic files in place, now we will start our actual conversion process.

For routes definition, i made 3 files:

1. urls.js file:

This file is simplest, it just contain url definitions:

export default {
    applyJob: '/job',
    browse:   '/job/:job_id'
};

2. RouteActions.js file:

This file will be used to navigate to different routes, it will replace import { Actions } from 'react-native-router-flux' with something like: import Actions from '../RouteActions'. And hence, will be on forefront for keeping the API same as router-flux’s Actions.

import { Platform } from 'react-native';
import history      from './RouteHistory';
import urls         from './urls.js';
import { generatePath } from "react-router";

export default class RouteActions {
  static pop() {
    setTimeout(() => {
      history.goBack();
    }, 1);
    return null;
  }

  static base(componentProps = {}) {
    history.push('', componentProps);
  }

  static post(componentProps) {
    const url = generatePath(urls.post, {postId: componentProps.postId});
    history.push(url, componentProps);
  }

  static posts(componentProps = {}) {
    history.push(urls.posts, componentProps);
  }

  static refresh(componentProps = {}) {
    history.replace(history.location.pathname, componentProps);
  }

  static replace(route, componentProps = {}) {
    history.replace(route, componentProps);
  }

  static jump(route, componentProps) {
    history.push(route, componentProps);
  }
}

Note that there are quiet a few extra routes , like pop, refresh, replace, jump and base, these routes are implemented as our app was using these routes a lot from react-native-router-flux, we tried to maintain as much interface as we could to match that of Actions from react-native-router-flux. And hence, we had to implement these functionalities.

Secondly, we are receiving componentProps and passing them in push and replace functions. This is also for the effort to match Actions behavior in react-native-router-flux, you will more details about this in Routes.js file description.

You might also have noticed that componentProps is optional in all other routes, but not in post function/route. Why?

This is because we are expecting an object will be passed, which will contain at-least postId, and can contain any other props it want to pass to the component being rendered o the route.

Take your time to try to make sense of this file. Then continue.

3. Routes.js file:

This file will be a replacement of old react-native-router-flux routes definitions , whether that was in a separate file or mixed up in App.js, this new file will replace those routes definitions and will actually be imported in App.js to render routes replacing router-flux’s import.

For the sake of simplicity, let’s assume there are only three routes we have to map:

  1. /posts (is home page after signin)
  2. /post/:post_id
  3. empty/root route which renders home page

It will look something like:

import React, {Component} from 'react';
import Routing, { Router } from './wrappers/Router/Router';
import history from './RouteHistory';
import urls from './urls.js';

//Screens
import Posts from './screens/Posts';
import Post from './screens/Post';
import Home from './screens/Home';

const { Route, Switch, Redirect } = Routing;

class Routes extends Component {
    render() {
        return (
            <Router history={history}>
                <Switch>
                    <Route exact path='' component={Home}/>
                    <Route exact path={urls.posts}
                        component={(props) => <Posts {...props} {...props.location.state}/>}
                    />
                    <Route exact path={urls.post}
                        component={(props) => <Post {...props} {...props.location.state}/>}
                    />
                    <Redirect to="/" />
                </Switch>
            </Router>
        );
    }
}

export default Routes;

We are importing our three screens and rendering them based on the url, lets break this down further:

Switch is a component which is used to render something based on some condition, it stops looking for matches whenever it finds first match, you can read docs for more info on this.

If we are on root path, without any sub paths, it will render Home component, as you can see in the code snippet.

If we are at /posts route, it will render Posts component, but that is a bit different, it is using render props pattern here, let’s discuss why.

You can notice we are spreading a couple things while rendering component:

component={(props) => <Post {...props} {...props.location.state} {...props.match.params}/>}

This is to match the react-native-router-flux‘s interface for navigating(which is something like

Actions.post({postId: 100, someOthrProps: 'value'})

  1. {…props} : this is spreading default props from react-router, this includes history, location and match(and maybe more). Please explore docs for details.
  2. {…props.location.state} this will spread any values we are passing when routing with Actions.post just like react-native-router-flux does, it calls this PARAMS in its docs. So from our example Actions.post({postId: 100, someOthrProp: 'value'}), post component will receive two props postId and someOthrProp. This is same concept from router-flux, if you are here reading this doc on replacing flux, I assume you have worked on router-flux and will understand what I am talking about here.

Lets use this new router

Now we can open our App.js file and start using this router, something like this(it is simplified a lot, of-course):

import React, {Component} from 'react';
import { View } from 'react-native';
import Routes from './Routes';

export default class App extends Component {
    render() {
        return (
            <View>
                <Routes/>
            </View>
        );
    }
}

Not to mention, you need to remove react-native-router-flux‘s import and initialization part in here.

Finally, replace imports:

At this stage, all your routing will be broken, you will be going to base/home route just fine, and any links or redirections will throw errors, thats because we have removed flux initialization from App.js and still trying to route using flux(in other components), lets start fixing our imports from:

import { Actions } from 'react-native-router-flux'

To:

import Actions from '../RouteActions'; //fix path obviously

And most routes should work, given that you have mapped all your urls in all 3 files, urls.js, Routes.js and RouteActions.js.

(This 3 file structure is obviously optional.)

Lookout and workout the gotchas and incrementally check and fix all links/redirects mapping routes in appropriate files.

Are we finished?

Not if you have deeplinking or universal links in your apps, in that case a little extra work might be required. We will not look into that here. you will have to figure that out yourself.

And we are done!

Thanks for bearing with me, do let me know if some parts in this article are hard to follow_(I understand there might be, but for someone who has worked properly with react-native-router-flux, this should not be hard to understand, still I am open to suggestions for improvement.)_

Cheers!

Discussion (0)