Sharing Features Between React Native and a Browser Extension in a Monorepo

Before joining Leather, I’d only ever worked in single-app repositories. At Qredo, the frontend was a standalone React app. At Cryptowatch, the same. Each project had its own repo, its own build pipeline, its own dependencies — clean boundaries, simple mental models. I knew monorepos existed, of course, and I’d read the arguments for and against, but I’d never had to navigate one day-to-day.

What surprised me most wasn’t the architecture itself — that part made intuitive sense once you saw it. It was the sheer surface area of things that could go wrong in the tooling layer. A dependency version mismatch in one package could cascade across the entire workspace. Build errors in one app could block CI for all apps. The feedback loops were longer and the debugging was more indirect than anything I was used to.

I joined Leather coming from single-repo React projects at Qredo and Cryptowatch. This was my first monorepo, my first time with React Native and Expo, and my first open-source project. The repo (leather-io/mono) was set up by the team before I joined — the structure, tooling decisions, and initial packages were already in place. My role was building features within that structure, and eventually contributing to the cross-platform sharing patterns.

Both apps needed the same features — collectibles gallery, transaction activity, token details — but the implementations were largely separate. Features were being built twice. Bugs were being fixed twice.

The packages/features Pattern

The team’s solution was a shared packages/features directory containing platform-agnostic feature logic. The key constraint: features can contain business logic, data mapping, and shared types, but not platform-specific rendering.

Each feature exports:

The platform apps then import these and wrap them with their own UI components.

The Activity Example

The biggest single expression of this pattern was #1837 — 152 files, +4,796/-1,678 lines. This PR:

Before this PR, we had the concept of OnChainActivity in our lists. Afterwards, all source Activity gets transformed to ActivityView for standardised UI rendering across platforms.

The PR has a video demo showing activity working on both mobile and extension simultaneously.

That PR took about two weeks of focused work, with several rounds of review. A 152-file change isn’t something anyone wants to review in one sitting, so I broke the review into logical chunks — the data layer changes first, then the UI integration, then the cleanup. The hardest part wasn’t writing the code; it was untangling the existing activity data flow enough to extract a clean ActivityView type that both platforms could consume without platform-specific logic leaking in.

There was a particularly frustrating stretch where the extension’s activity list was rendering correctly but the mobile app’s was showing stale data after the refactor. It turned out to be a React Query cache key mismatch — the mobile app was using a slightly different query key structure, so the shared hooks were populating a different cache entry than the mobile UI was reading from. Small bug, but it took longer to find than it should have because the symptoms were subtle.

The Collectibles Example

I built the collectibles UI on mobile first (Sep-Nov 2025), then extracted the shared logic into packages/features (#1981), and worked on bringing it to the extension (#2067). The underlying query hooks and API services were built by other team members — my contribution was the UI layer and the cross-platform extraction.

The CollectibleView type became the contract between the data layer and each platform’s renderer. Mobile renders it with React Native’s Image and FlatList. The extension renders it with HTML img tags and CSS grid. Same data, different presentation.

Build System: Where The Pain Lives

Sharing code across platforms in a monorepo sounds clean in theory. In practice, the build system is where everything breaks.

As someone new to monorepos, these were the hardest problems. The architecture pattern itself was straightforward — the tooling to make two different build pipelines work with shared packages was not.

The gotcha that caught me most off guard was pnpm’s dependency hoisting behaviour. In a single-repo project, if a package is installed, it’s available — simple. In a pnpm workspace, packages are symlinked and hoisted according to rules that aren’t always obvious. We had a situation where a shared package imported a dependency that was installed in the mobile app but not declared in the shared package’s own package.json. It worked locally because pnpm hoisted it, but it broke in CI where the hoisting configuration was stricter. The fix was trivial — add the missing dependency declaration — but the debugging was not, because the error message pointed to a completely different package.

My advice for anyone setting up their first monorepo: invest in your CI pipeline early and make it strict. Your local environment will paper over dependency issues that only surface in clean installs. Run pnpm install --frozen-lockfile in CI, enable strict peer dependency checks, and treat any “works on my machine” discrepancy as a bug in your workspace configuration, not a CI fluke.

The Go-Live Moment

After weeks of feature-flagged development, #2067 went live with the new extension home tab layout. This was the payoff for the whole team: a feature that took months to build on mobile was adapted for the extension in a fraction of the time because the data layer was already shared.

What I Learned From My First Monorepo

  1. The real cost isn’t the architecture — it’s the build system. Getting EAS, webpack, and pnpm to play nicely together consumed more time than the actual feature code.
  2. Share data, not UI. View types as the contract between shared logic and platform-specific rendering worked well. Trying to share React components across React Native and React DOM would have been a nightmare.
  3. Build one platform first, extract second. Building mobile first and then extracting shared code was much easier than trying to build the shared layer abstractly.

Key PRs

PRWhatStatsVideo?
#1981Shared feature utilities foundation+973/-111, 33 files
#1837Activity refactor + extension NFTs+4,796/-1,678, 152 filesYES
#2067Go live: new extension home+284/-99, 19 files
#2018Fix EAS mobile build+1,039/-15, 14 files
#1824Setup new Home component+213/-100, 7 files