Smart Code Sharing for Streamlined React Development: An Inside Look
Maintaining efficiency and minimizing repetition in multi-platform development can be a challenge. Discover how we leaned into code reusability to accelerate development of an enterprise React Native application and how to use code sharing overall to fast track development and maximize efficiency.
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.
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.
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 natively.
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.
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.