DEV Community

Cover image for Driving Towards a Universal Navigation Strategy in React
Matheus Albuquerque
Matheus Albuquerque

Posted on • Edited on • Originally published at ythecombinator.space

Driving Towards a Universal Navigation Strategy in React

When I joined STRV, they had a specific request for me; to build a front-end app for iOS, Android, and Web, sharing component and business logic among all the platforms.

Since I’m a front-end developer who loves new areas, I couldn’t say no, and I had to jump at the opportunity.

I ended up facing a variety of challenges; from lack of real-world-scenarios content related to React Native Web to unexpected lack of documented stuff on popular projects, to struggling to build some platform-specific modules.

And this post is focused on – a very important – part of this journey: building a navigation solution.

But first…

A bit of context

I had only worked on an example React Native app before (uncompiled and unpublished). At the time of this project, I didn’t know much about React Native, to be honest.

I first heard of Expo and its experimental web support1 but I decided not to go for it mostly because I enjoy having control over the project stack and being aware of what's happening; I want to be able to customize the installation, install custom versions of modules and have more control of project dependencies.

I then heard of two other initiatives on Github: ReactNative for Web and ReactXP. Both share similar goals but the approaches differ. As the official documentation for ReactXP states:

ReactXP is a layer that sits on top of React Native and React, whereas React Native for Web is a parallel implementation of React Native — a sibling to React Native for iOS and Android.

This post won't focus on covering the differences between these two but, after going through some technical blog posts and talks, we ended up going for ReactNative for Web.

After a bit of digging into articles and trying to implement each environment in its own realm, I found that for me, the best starting point was a great template, called react-native-web-monorepo2, which brings support for universal apps using a little help from Yarn Workspaces.

Before starting to implement this approach into your project, though, I would suggest reviewing your requirements and checking whether these tools solve all of your needs.

What we have out there

Some popular routing solutions on the React.js ecosystem were not meant to support both DOM and native environments; <div>s are different from <View>s, <ul>s are different from <FlatList>s and most of the web primitives are different from the mobile ones – which makes it difficult to come up with a universal solution. @reach/router is one example of web solutions that have opted not to face the challenges of supporting both environments.

As of now (January 2020), though, we have a few ready universal web/native formulas. But they all ended not serving completely our needs:

  • react-router is a great option for the web, but when on mobile, it lacks screen transitions, modals, navbar, back-button support, and other essential navigation primitives.
  • react-navigation suits great on mobile but given its web support is still considered to be experimental – and has not yet been widely used in production – it's very likely you're going to face a few issues3 related to history and query parameters. Also, it lacked TypeScript typings – which made me write part of the definitions on my own since TypeScript was a must-have for the project.

And this brings us to the next part!

Thinking of a solution

The code from this post is available on GitHub: ythecombinator/react-native-web-monorepo-navigation

I admit one of the most puzzling things when we dived into this journey was not being able to find how popular apps out there using React Native for Web (e.g. Twitter, Uber Eats and all the others mentioned here) are doing navigation – and how they faced challenges like the ones I mentioned before.

So we had to work on our own!

Our new solution was based on abstracting on top of the most recent releases of react-router-dom4 and react-navigation5. Both evolved a lot and now them two seem to share a few goals which I consider to be key-decisions for properly doing navigation/routing in React:

  • Hooks-first API
  • Declarative way to implement navigation
  • First-class types with TypeScript

Given that, we came up with a couple of utils and components which aim a universal navigation strategy:

utils/navigation

Exposes two hooks:

  • useNavigation: which returns a navigate function that gets a route as a first param and parameters as other arguments.

It can be used like this:

  import { useNavigation } from "../utils/navigation";
  // Our routes mapping – we'll be discussing about this one in a minute
  import { routes } from "../utils/router";

  const { navigate } = useNavigation();

  // Using the `navigate` method from useNavigation to go to a certain route
  navigate(routes.features.codeSharing.path);
Enter fullscreen mode Exit fullscreen mode

It also provides you with a few other known routing utilities like goBack and replace.

  • useRoute: which returns some data about the current route (e.g. path and params passed to that route).

This is how it could be used to get the current path:

  import { useRoute } from "../utils/navigation";

  const { path } = useRoute();

  console.log(path);

  // This will log:
  // '/features/code-sharing' on the web
  // 'features_code-sharing' on mobile
Enter fullscreen mode Exit fullscreen mode

utils/router

This basically contains a routes object – which contains different paths and implementations for each platform – that can be used for:

  • Navigating with useNavigation
  • Switching logic based on the current route with useRoute
  • Specifying the path – and some extra data – of each route rendered by the Router component

components/Link

It provides declarative navigation around the application. It is built on top of Link from react-router-dom on web and TouchableOpacity + useNavigation hook on mobile.

Just like Link from react-router-dom, it can be used like this:

import { Text } from "react-native";

import { Link } from "../Link";
import { routes } from "../utils/router";

<Link path={routes.features.webSupport.path}>
  <Text>Check "Web support via react-native-web"</Text>
</Link>
Enter fullscreen mode Exit fullscreen mode

components/Router

This is the router itself. On the web, it's basically a BrowserRouter, using Switch to pick a route. On mobile, it's a combination of both Stack and BottomTab navigators.

Wrapping up everything above, what you get is going through each screen of the app and seeing how useRoute(), useNavigation() and <Link /> can be used regardless of the platform you are.

If I was asked about future work on this, I'd mention as next steps:

1) Adding more utilities – e.g. a Redirect component aiming at a more declarative navigation approach6

2) Tackling edge cases on both platforms

3) Reorganizing most of the things inside a navigation library and leaving only the main Router component and utils/router to be written on the application side.

Conclusions

My feeling is that web, mobile web, and native application environments all require a specific design and user experience7 – and by the way, this matches the mentioned “learn once, write anywhere” philosophy behind React Native.

Although codesharing is a great advantage to React and React Native, I'd say that it is very likely that shared cross-platform code should be:

  • Business Logic
  • Config files, translation files, and most constant data – those that are not render-environment-specific
  • API / Formatting; e.g. API calls, authentication and formatting of request and response data

A few other layers of the app, like routing, should use a library that is most appropriate for the platform, i.e. react-router-dom for web, and react-navigation or similar for native.

Perhaps in the future, we can have a truly unified code base, but for now, it doesn't feel like the technology is ready and the approach shared here seemed to be the most suitable one.

Footnotes

1) There's an amazing talk by Evan Bacon on Expo for Web this year at Reactive Conf – if you haven't checked it out, I really recommend you to.

2) This one was authored and is the same used by Bruno Lemos, the author of DevHub, a Github client that runs on Android, iOS, Web, and Desktop with 95%+ code sharing between them. If you're interested in how he came up with this solution, check this out.

3) These issues include:

  • Functionality-wide
    • Query parameters from URL not passed down (here)
    • Pushing back not working (here and here)
    • Some params pushed from one route to the other for convenience being encoded to the URL
  • Developer-experience-wide
    • Lack of TypeScript typings (here) – which made me write part of the definitions on my own

4) React Router v5 focused mostly on introducing structural improvements and a few new features. But then v5.1 brought a bunch of some useful hooks which allowed us to implement the mentioned ones for the web.

5) React Navigation v5 also did many efforts for bringing a modern, hooks-first API, allowed us to implement the mentioned ones for mobile.

6) There's a very good post about doing declarative and composable navigation with <Redirect /> here.

7) If you're interested in this topic, in this talk I share a couple of lessons learned while building an app with code sharing as a primary objective — from project setup, through shared infrastructure, all the way up to shared components and styling — and how you can achieve the same thing.

Top comments (5)

Collapse
 
codypearce profile image
Cody Pearce

Good post!

react-router is a great option for the web, but when on mobile, it lacks screen transitions, modals, navbar, back-button support, and other essential navigation primitives.

react-router does have some solutions for native-like screen transitions, for example react-router-native-stack, though obviously not as well supported as the ones for react-navigation.

Personally, I don't think routing libraries should be providing "navigational UI components" like modals, navbar, and drawers. These are UI components not router components, they just happen to sometimes contain buttons/links that trigger navigation. This makes the router library more complex and usually doesn't give developers ultimate control over how "navbars" and other components render.

Collapse
 
ythecombinator profile image
Matheus Albuquerque

Thanks for the feedback, sir! 😊

I see your point and probably I was not very clear on the post but I do agree with you.

Actually, when pointing out react-router-native doesn't provide screen transitions and navigational UI components I didn't mean that these should be coupled to the core routing; it's more like there should be @react-router-native/core and then @react-router-native/stack, @react-router-native/drawer and so on, just like react-navigation does – and, as far as I am concerned react-router-native-stack is not an official one.

Collapse
 
satya164 profile image
Satyajit Sahoo • Edited

I think the difference is "routing" libraries vs "navigation" libraries. I agree, the routing library should not provide any UI.

But in case of React Native, where we are building apps which usually need to match the native experience provided by native apps, it's also wasteful to redo these for every app. In a lot of cases, you "do not" need ultimate control over how navbars and other components render, but want to provide a native experience to the user and have ability to customize the UI enough to match your design. That's what a navigation library aims to achieve.

React Navigation is essentially an ecosystem, not a single library. The @react-navigation/core package provides the core logic and is equivalent to a routing library when combined with @react-navigation/routers. Then there are integrations such as @react-navigation/stack, @react-navigation/bottom-tabs etc. which provide UIs on top of this router logic to make app development easier.

You can always build these things yourself, but if you want to provide a native experience, it's not that trivial. You need to deal with complex animations and gestures etc (look at the header animation on iOS). I have built these things, they take a lot of time.

Sure, on web it works. You can build a great web app with just a router. But people rarely have gestures and animations in web apps. Native apps have a higher standard when it comes to animations and gestures, which makes it much more work.

Collapse
 
ypedroo profile image
Ynoa Pedro

Great post!

Collapse
 
ythecombinator profile image
Matheus Albuquerque

Thanks 🎆