Skip to content
Mehedi Hasan Sagor

// blog

Sharing UI Components Between Ionic and React Native with a Monorepo Approach

Mehedi Hasan Sagor Oct 6, 2026 6 min read
Sharing UI Components Between Ionic and React Native with a Monorepo Approach

Why a Shared Component Strategy Matters

When a startup needs both a web‑first progressive app (Ionic) and a native mobile experience (React Native), the temptation is to duplicate UI work. Two separate codebases mean two sets of bugs, two design updates, and higher long‑term maintenance costs. A shared component library solves that friction by giving you:

  • Consistent branding – one source of truth for colors, typography, and interaction patterns.
  • Faster iteration – change a component once, ship to web and native simultaneously.
  • Reduced technical debt – fewer divergent implementations to keep in sync.

The challenge is that Ionic is built on top of Angular (or Vue) while React Native uses React. Directly importing Angular components into React Native isn’t possible, but we can compile framework‑agnostic web components that both environments can render. Stencil, a compiler for standards‑based web components, fits perfectly.

Setting Up the Monorepo

A monorepo keeps all related packages under one version control root, making dependency management trivial. We’ll use Nx because it supports both Angular (for Ionic) and React Native workspaces out of the box.

# Install Nx globally (optional)
npm i -g nx

# Create a new workspace
npx create-nx-workspace mobile-shared --preset=empty

cd mobile-shared

# Add the needed plugins
npm i -D @nrwl/angular @nrwl/react @nrwl/react-native @nrwl/js

The folder layout will look like this:

mobile-shared/
├─ apps/
│  ├─ ionic-app/          # Angular + Ionic project
│  └─ rn-app/             # React Native project
└─ libs/
   └─ ui-components/      # Stencil‑based web components

Adding Stencil to the Workspace

Stencil isn’t a first‑class Nx plugin yet, but we can treat it as a generic JS library.

# Generate the library
nx generate @nrwl/js:library ui-components --directory=libs --publishable

# Install Stencil and its peer deps
npm i @stencil/core @stencil/angular-output-target @stencil/react-output-target

Create libs/ui-components/src/components/button/button.tsx:

import { Component, Prop, h } from '@stencil/core';

@Component({
  tag: 'app-button',
  styleUrl: 'button.css',
  shadow: true,
})
export class AppButton {
  /** Button label */
  @Prop() label: string = 'Click me';

  /** Emits when the button is clicked */
  @Prop() onClick: () => void;

  render() {
    return (
      <button onClick={this.onClick}> {this.label} </button>
    );
  }
}

Add a simple style in button.css:

button {
  padding: 0.5rem 1rem;
  font-size: 1rem;
  border-radius: 4px;
  border: none;
  background: var(--primary-color, #007bff);
  color: #fff;
  cursor: pointer;
}

Generating Framework Bindings

Stencil can output wrappers for Angular and React, allowing us to consume the same web component with idiomatic syntax.

Angular (Ionic) Wrapper

Create libs/ui-components/angular-output.ts:

import { defineCustomElements } from '@my-org/ui-components/loader';
import { createAngularComponent } from '@stencil/angular-output-target';

// Register the custom elements once (e.g., in main.ts)
defineCustomElements();

export const AppButtonComponent = createAngularComponent('app-button');

In the Ionic app module (apps/ionic-app/src/app/app.module.ts):

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { IonicModule } from '@ionic/angular';
import { AppComponent } from './app.component';
import { AppButtonComponent } from '@my-org/ui-components/angular-output';

@NgModule({
  declarations: [AppComponent, AppButtonComponent],
  imports: [BrowserModule, IonicModule.forRoot()],
  bootstrap: [AppComponent],
})
export class AppModule {}

Now you can use <app-button> in any Ionic template just like a native Angular component.

React Native Wrapper

React Native cannot render a DOM element directly, but Stencil’s React output gives us a regular React component that forwards props. To bridge that into native code we use react-native-webview to host the web component inside a tiny WebView. This adds a few kilobytes but keeps the UI consistent.

Install the WebView package:

npm i react-native-webview

Create libs/ui-components/react-output.tsx:

import React from 'react';
import { WebView } from 'react-native-webview';
import { defineCustomElements } from '@my-org/ui-components/loader';

defineCustomElements();

export const AppButton = (props: { label: string; onPress: () => void }) => {
  const html = `
    <html>
      <head>
        <script type="module" src="${require.resolve('@my-org/ui-components/loader')}"></script>
      </head>
      <body>
        <app-button label="${props.label}"></app-button>
        <script>
          const btn = document.querySelector('app-button');
          btn.addEventListener('click', () => window.ReactNativeWebView.postMessage('pressed'));
        </script>
      </body>
    </html>`;

  return (
    <WebView
      originWhitelist={['*']}
      source={{ html }}
      onMessage={(e) => {
        if (e.nativeEvent.data === 'pressed') props.onPress();
      }}
      style={{ height: 50, width: '100%' }}
    />
  );
};

In a React Native screen (apps/rn-app/src/screens/HomeScreen.tsx):

import React from 'react';
import { View, Text } from 'react-native';
import { AppButton } from '@my-org/ui-components/react-output';

export const HomeScreen = () => (
  <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
    <Text>Welcome to the RN app</Text>
    <AppButton label="Tap me" onPress={() => alert('Button pressed!')} />
  </View>
);

Development Workflow

With Nx, you get powerful affected‑project detection. Run nx serve ionic-app to start the Ionic dev server; any change in libs/ui-components triggers a rebuild of the Stencil library and hot‑reloads the Ionic UI.

For React Native, you can use the standard Metro bundler:

nx run rn-app:start

When a component changes, Stencil recompiles the web component and the WebView reloads the HTML automatically because the source prop receives a new string.

Linting and Type Safety

Add ESLint and Prettier at the root:

npm i -D eslint prettier eslint-config-prettier eslint-plugin-import
npx nx g @nrwl/linter:eslint-project ui-components

Because Stencil is TypeScript‑first, all props are strongly typed. The generated Angular and React wrappers inherit those types, so you get compile‑time safety across both platforms.

Trade‑offs and When Not to Use This Pattern

| Aspect | Benefits | Drawbacks | |--------|----------|-----------| | Performance | Web components render natively in the browser (Ionic) and inside a lightweight WebView (RN). | The RN WebView adds a small overhead; for ultra‑smooth 60 fps animations, a pure React Native component may be faster. | | Tooling | Single source of truth; Nx caches builds, reducing CI time. | Requires developers to be comfortable with Stencil and WebView quirks. | | Native Features | Can expose native APIs via custom events and bridge code. | Complex native integrations (e.g., camera, Bluetooth) still need platform‑specific wrappers. | | Bundle Size | Shared code reduces duplicated libraries. | Stencil runtime (~10 KB) plus WebView code adds to the RN bundle. |

If your product’s core experience is heavy on native animations or requires deep platform APIs, you might keep those screens native and only share high‑level UI pieces (buttons, cards, forms) via this approach.

Getting It Into CI/CD

  1. Run lint and unit tests for the Stencil library first:
    nx run ui-components:test
    
  2. Build the library and publish it to a private npm registry (or use workspace linking):
    nx build ui-components
    
  3. Build each app with its own pipeline step. Because Nx knows dependencies, a change in ui-components automatically marks both ionic-app and rn-app as affected.
  4. Deploy Ionic to Netlify/Vercel (static site) and bundle React Native with Fastlane for iOS/Android.

By treating the UI component library as a first‑class artifact, you guarantee that every release of either front‑end consumes the exact same version, eliminating drift.

Takeaway

A monorepo powered by Nx, with Stencil‑generated web components, gives startups a pragmatic way to share UI between Ionic and React Native without sacrificing type safety or developer velocity. The upfront setup cost is modest—install a few packages and add wrapper code—but the payoff is a single, testable component set that lives in one place, ships faster, and keeps branding consistent across web and native.

Start by creating the monorepo, building one simple component, and then iterate. You’ll quickly see the reduction in duplicated effort and the confidence that a UI change truly propagates everywhere.

  • Ionic
  • React Native
  • Mobile
  • Monorepo
  • Typescript