Smart Code Sharing for Streamlined React Development: An Inside Look

This article delves into the journey of leveraging code reusability to accelerate the development of Purple Carrot's React Native mobile application. By abstracting types and business rules from presentational components, we achieved code sharing capabilities between the existing React web and the new React Native mobile application, regardless of the UI rendering differences between them.

We will explore the following so you can apply our insights to your own software project:

The Background: Migrating to React

Purple Carrot is a large, plant-based meal delivery service and a long-time client partner of Whitespectre. They have a highly custom front-end experience and feature set. For several years, we built and maintained a server side rendered Rails application that worked well for them. However, as the company expanded their offerings and feature set, a more dynamic client side rendered application was a better fit.

That’s where it all started. Taking the existing Rails backend as the foundation, we developed a strong API and began migrating key parts of the core Rails application to a frontend rendered TypeScript React app.

Building with Reusability in Mind

With the idea of a future mobile app on the horizon, we designed both the API and the TypeScript React app with the right level of abstraction for a multi client architecture, always with reusability and efficiency in mind.

A Strong and Versatile API in the Backend

The strategic decision to build a robust API not only catered to the needs of the TypeScript React app, but also laid the groundwork for potential future frontend clients. 

The development process involved careful consideration of security and authentication mechanisms. Implementing industry-standard protocols like JWT enabled secure and authorized access through token based authentication for non-web clients. Thorough documentation of the API endpoints and data structures facilitated future development efforts and integration with other potential frontend clients.

Abstracting Types and Business Rules in Frontend

We implemented a variation of the MVC pattern where models encapsulated as much business logic as possible, leaving just the presentational logic in the React views. By decoupling the core logic from the view, our models became agnostic to the view rendering library.

TypeScript also played a role in making these models easily interoperable. In addition to encapsulating logic and doing required data transformations, every attribute in models was well typed and we created interfaces and types for every data piece.

When the idea to develop a mobile application arose, this strategic approach allowed us to reuse the logic in the models, ensuring maximum development efficiency and reducing redundant efforts, even if view rendering in mobile was completely different from HTML and CSS.

Sharing Code between React and React Native

At this point, and after a year of a progressive and successful rollout of the new React frontend, Purple Carrot started thinking about a mobile application to complement their web experience.

Given our expertise in React Native, this was a natural choice for us. Another TypeScript client would also give us the chance to prove our code sharing hypothesis, and although this was initially an iOS only app, supporting Android was planned in the future.

When initial wireframing and designing efforts started, it became clear that user interface design had to be different on mobile for an optimum user experience. No surprise here, but this also validated our thoughts on what’s worth sharing versus what’s not. You can’t share UI components between React and React Native because they render differently, but most of those presentational pieces were already going to be different by design, so most of them would need to be rewritten anyway.

Nevertheless, we did have a strong foundation of models and types in the React application, and those were already encapsulating most of the non-presentational application logic. We imported models from the React app repository into the new React Native application and we started to use them, along with the existing API.

The outcome was a significant saving in development time and effort. We had to implement some new API endpoints and model features for the new functionality in the mobile app, but most of it was analogous in web and mobile. That's the case, for example, for cart, upcoming menus or history views.

Having different teams share part of their codebase introduced new challenges. Establishing coordination between them was important to make this approach work in the long run. We organized work in three teams:

  • The Core Team: Responsible for the backend and API.
  • The Web Team: Responsible for the web application and shared models.
  • The Mobile Team: Responsible for the mobile application.

When a model or API update is required, the mobile team issues a feature request to the web, core or both teams. Coordination calls have been established and we avoid breaking changes as the rule of thumb, but we have comprehensive API documentation, API and model versioning and even coordinated releases if required.

Benefits of this Approach

By sharing code between the mobile and web applications, we established a single source of truth for frontend business rules. When a frontend behavior changes, like a validation rule or an alert notification in cart, there is only one place in code to update. This streamlined the development process, reduced complexity and eliminated the need for redundant updates and synchronization between different codebases.

This was demonstrated with some of the most recent business rule changes that were made. Meal selection in Purple Carrot’s cart is limited by some rules, and one of them is related to the box size. They were using abstract point values until now, but they decided to transition to more accurate volume metrics for selecting box sizes. This showcased the benefits of this approach, as we just had to update a single model to make it work in both applications.

On the other hand, UI and presentational changes, that are frequent both in web and mobile, are still totally independent. This gives us total freedom to work out the different user experiences we want, tailored to the specifics of each platform.

Other Possible Approaches

Despite that, it is still technically possible to share UI components between React and React Native. The react-native-web project is an implementation of React Native components that is compatible with React DOM in the browser, acting as an abstraction layer for rendering differences in both platforms. However, there are still a few caveats to keep in mind:

  • You can render an already existing React Native component in a React web application, but not the other way around. If you want to write cross platform components, you have to write them in React Native from the beginning.
  • Styling is also different in React Native. You won’t have cascading styles, but rather a CSS-in-JS solution with scoped styles, where media queries, selectors, pseudo-selectors, and pseudo-elements are not supported. Thus, responsiveness and hover states can be tricky to manage.
  • Using 3rd party packages that contain native modules can cause issues. APIs for BLE or camera are also completely different in web and mobile, so you will probably need to write platform specific code.

Although the project is pretty mature and other libraries like HTML Elements and React Native Elements have emerged to ease development, we think that shared components are a niche requirement. In our experience at Whitespectre, UI design differences between platforms make them irrelevant most of the time, and more difficult to maintain in the long run.

They are still useful if you want to port a specific React Native component to a React web application without rewriting it, especially if it’s going to look exactly the same. But probably not as an integral approach to a multi client platform development.

Learnings from this Process

This was an interesting journey, and while code sharing proved to be beneficial, certain challenges and considerations emerged throughout the process: 

Isolating Web-Specific Code and Patterns

It’s easy to write browser specific frontend code without realizing. In models, references to the window object or other specific browser APIs have to be avoided. This may also need different TypeScript config files for your clients and excluding browser code depending on your repo structure.

Authentication Differences

Supporting token-based authentication with refresh tokens requires careful implementation. The existing authentication was based on cookies, and our web application still uses them. But if you want to support non-web clients in an authenticated JSON API, you will need to implement support for both.

Using Webviews to be Agile

Progressive feature implementation was a must for us. We wanted to release the MVP with the key features fast, but still be able to access other features before we could implement them native​​ly.

Webviews were the answer. We made important features beyond the MVP available through them. This introduced unique complexities, like implementing communication between our web and mobile apps through events and webview detection in the React web app.

Organizing our Repository

The mobile application was not in our initial plans, so models were coupled to the web app and in the same repository. Extracting the models to a shared repository is a good idea, although you could start by importing everything and just ignore the web specific code in your mobile repository configuration.

Depending on project specifics, a monorepo structure could also be beneficial in shared code setups. In that case, having web, mobile and a shared folder with models and assets would be the way to go.

The Importance of Team Coordination

Effective communication and collaboration between teams is crucial. Having a shared codebase also means, on the other side of the coin, that breaking changes propagate to all the clients too.

Sometimes, an unexpected change that is working fine in one client could break things in the other. Having a clear and well established team collaboration process, good documentation and versioning is important to avoid these problems or address them if they happen.

Final Thoughts

Sharing code between Purple Carrot's React web and React Native mobile applications has proven to be highly beneficial. It streamlined the development process and minimized repetition, improving efficiency while retaining enough freedom to implement differentiated user experiences.

This journey has not been without its challenges, but in our experience at Whitespectre, the outcome demonstrates the immense potential and advantages of code sharing in accelerating development while still delivering exceptional user experiences. As React Native continues to evolve, further exploration and refinement of these practices will optimize app development processes and drive innovation in the field.

Let’s Chat