How We Use Code Sharing To Make Feature Development Easier
In the beginning: The desktop vs. mobile race
Float was first launched in 2012 as a web application. In June 2018, we released the first mobile version of Float on iOS, and in 2019 we released a native app for Android.
In the early days of the Float mobile app, our focus was to pick the most important features for mobile users and make them accessible on iOS and Android devices. We built the mobile app using React Native, as it was a library we were familiar with and we knew it could support both device platforms. I wrote about our first build with React at the time.
Over time, our feature set for the Float web application has continued to grow. In September 2019, we rebuilt the schedule using React Hooks—which increased speed and performance for larger teams—and in March 2020, we launched time tracking—which gave teams the ability to log hours and compare them with their scheduled tasks. If you add it all up, we've had about 40 feature release cycles since our iOS app was first launched!
Naturally, as the web app has continued to add new features, we've received a growing number of requests for more functionality on the mobile app as well. Users began asking for more task scheduling options, notification updates, and the ability to log time on the go. Features that once seemed like "nice-to-haves" on mobile, were quickly becoming necessities. At times it felt like a race between the two platforms, albeit with unequal conditions. Our desktop web app was like an F1 car that had years of tuning and experience 🏎️, while our mobile app was a budding kart car just trying to keep pace 🚗.
With the knowledge that we already had a best-in-class web app experience, we started to investigate and analyze what code efficiencies we could replicate on mobile. We were keen to find out if we could borrow some lines of code from the web app as opposed to implementing them from scratch on mobile.
While both apps are based on React, there are still a number of differences between them. The renderer is different (React DOM for web and React Native for mobile), the platform APIs are different (browser vs. React Native APIs), and the styling rules and approaches are different (styling for big screens with complete freedom vs. a strict minimalistic layout for touchscreens). Moreover, styles for the web app almost never inline into React components, where it’s the main rule for React Native apps.
Ultimately, instead of trying to find distinctions between the two, we decided to focus on the similarities.
For example, the Float web app has a logic for calculating tasks' repeated count. When you select a repeat mode and set a repeat end date, you can easily see how many times the task will be repeated. This logic is based on various inputs, including repeat type (e.g., weekly, once per 2 weeks, monthly, etc.), the number of weekends and holidays during the period, and the day of week.
We’ve reused a lot of helpers—time and date manipulation, form validation, label formatting for the activity feed, sending API requests, handling WebSockets, and much more. Many pieces were also covered by unit tests, which have essentially allowed us to double our test coverage without adding a single line of code!
Shared libraries: A consistent UI
As both applications are based on React, React Components helped us to abstract away platform-specific implementation details.
At the same time, we understood that React Components on the lower level should be distinguished. While the React DOM (web) approach requires us to use HTML elements like div, span, and h1, the approach for mobile is based on the React Native entities View, Text, FlatList, etc.
Moreover, styles are set entirely differently. React Native uses an inline styles approach, while in the web app, styles are injected as a style tag for the page. We considered using React Native Web, but it didn't cover our needs, as we wanted to render web components in the React Native environment.
Instead, we came up with our own abstraction. We created shareable component fabrics, and each platform has to initialize it with its own elements following the fabric API.
When passing styled elements, we are delegating "consumers" (web and mobile) to be responsible for styles and handle business logic in a sharable place.
We can define the public API with typescript or React prop types, so all-important components will be passed.
State management: Using Redux on web and mobile
For the web application, we've used Redux as a centralized state management library. We relied on redux-thunk for handling side effects and Reselect to compute derived data with efficient caching.
In the first mobile app iteration, we used GraphQL based on Apollo, maintained a separate server for requests aggregation, and fetched data via Apollo Client. It wasn't until later on that we realized we needed to handle data more efficiently for larger companies. Some users have thousands of tasks, people, and projects, so data processing performance is key!
Since we already had an efficient way to handle significant amounts of data for the web application, we decided to use the same approach for the second version of the mobile app. We rethought the user experience, used UI for animations, switched to bare Expo workflow, bumped many dependencies, and more.
We also took the opportunity to rethink the state management approach. We could either spend months building the new state management structure or reuse what we already had for the web. We decided to say goodbye to the GraphQL approach and improve the consistency between platforms, which, surprisingly, was easier than expected. By ejecting Redux entities to the shareable library, all we had to do was import actions, action creators, and reducers and selectors for both platforms, wrap it with ReduxProvider, and initialize the store locally.
Sharing code: Organizing platform-specific code sharing
One of the trickiest things was understanding which modules were needed for mobile since it still has a bit of simplified functionality. React Native offers a way to organize and separate code by platform, which helped us to define two modules—a regular one and one with the ".native" extension. The web version uses the original module after React Native tries to find ".native" first.
We believe that this optimization can be skipped for smaller redux structures since the redux entity is usually simple and doesn't noticeably increase bundle size. Runtime also isn't affected since the mobile part doesn't call any of the unrelated methods.
It was a big challenge organizing the code-sharing correctly. We had a few requirements that needed to be met:
- Local development could not suffer. Developing features usually affects both common and platform-specific codebase. We wanted to be able to start the application locally and have all development velocity improvements (like HMR and fresh types) unaffected even if you changed a sharable part.
- Testing and releasing changes in conjunction. We didn't want to wait for the dependency update chain. For example, updating a common part shouldn't force us to release it before testing it with web and mobile parts. Instead, we wanted to test everything in a single PR.
- Reusing the same dependencies between platforms. Each package potentially could have a lot of the same dependencies. We wanted it to be installed only once to avoid needing three separate "npm install" processes.
Publishing packages as separate npm dependencies didn’t work for us, so we started to look into the monorepo approach and found Yarn workspaces. It provided a way to set up the full package architecture, including installing all relevant dependencies, resolving duplicates, and bootstrapping the ones within the project by using a single "yarn" command.
Shared advantages: A single source of truth that reduces risk
Since revamping our approach and creating sharable parts between web and mobile, we've noticed many advantages when adding new features to Float.
Not only have we minimized the time it takes for an implementation, we've also decreased the risk that something could go wrong. If something does go awry, we only need to fix it once! Our new approach also provides a better way of calculating total hours, placing the next repeating task, and handling the API response.
The only real negatives are that organizing the monorepo can sometimes take time. That's not a big issue with Yarn though, as the configuration is well documented and best practices are widely spread within the community. You also have to be careful when making a change to the common part, as you can break both platforms at once. If you have E2E tests written for both platforms and the QA process, however, it shouldn’t cause too much pain.
Overall, we highly recommend sharing your code between different platforms. It might seem difficult at first to find sharable places and align different technologies, but the advantages of doing so are great in the long term.
The race between our web app and mobile app is officially over 🏁. We've delivered a faster and more intuitive mobile app, with all the features from the web version of Float that you need to be successful on the go! Download it for iOS and Android today.
Get exclusive monthly updates on the best tools and productivity tips for asynchronous remote work
Join 100,000+ readers globally