One of the greatest advantages of using Stencil to build a design system is that Stencil components are compatible with multiple different frontend frameworks. This means that you can write one core component library with Stencil and generate multiple component libraries for different frameworks based on that core. These components can then be consumed by a variety of apps using a variety of frameworks.

With all these different component libraries and applications depending on each other, you’ll want a way to organize your packages and streamline your development workflow. For this kind of setup, organizing your packages under a monorepo can be incredibly helpful. A monorepo is a single repository that contains all of the code/packages for your project. Nx is a powerful build system for building monorepos.

In this tutorial, we’ll use Nx to create a monorepo that includes React and Angular component libraries derived from a single Stencil project. We’ll also create React and Angular applications that consume these components, and we’ll see how all of these packages work together under one monorepo.

You can find all of the code for this tutorial in the nx-stencil github repo. If you prefer to watch a video tutorial, I recently created a video where I walk through this entire process. Check it out!

Create an Nx Workspace

The first thing we need to do to get started with a monorepo is create an Nx workspace. We can do that by running

npx create-nx-workspace@latest

After running this command, you’ll be prompted for a workspace name. I’m going to call mine “nx-stencil” for this tutorial. After you name your workspace, you’ll be prompted to choose what you want to create in the workspace. I’m going to choose “core” for the most basic setup. Finally, you’ll be asked if you want to use Nx Cloud, which we won’t need for this tutorial.

NOTE: Many of the commands in this tutorial will involve using the Nx CLI. You may want to install the Nx CLI globally ahead of time by running npm install -g nx.

Adding a Stencil Project

With our Nx workspace set up, we can now add a Stencil project to our workspace. First, we’ll add the Stencil Nx plugin as a dev dependency by running

npm install @nxext/stencil --save-dev

The Nx Stencil plugin provides a lot of commands that simplify some of the most common aspects of building a monorepo around a Stencil project. With the plugin installed, we can create our stencil project by running

nx g @nxext/stencil:lib core-components --buildable

NOTE: You may receive an error indicating that you have to install @nrwl/cypress first. You can do that by running npm install @nrwl/cypress --save-dev.

When you run this command, you’ll be prompted to pick a stylesheet format. After that, the plugin will create our stencil project, core-components, in the packages directory. This will be a standard Stencil project with a my-component component already created. We’ll be using this component to demonstrate how to use a Stencil component in both a React and Angular app.

Creating a React Component Library

One of the major advantages of Stencil is that we can wrap our Stencil components to work seamlessly with different frontend frameworks. Better yet, the Nx Stencil plugin comes with a command to simplify this process. To create a library of React components from our Stencil components, we can run the following command:

nx g @nxext/stencil:add-outputtarget core-components --outputType='react'

NOTE: You may receive an error indicating that you have to install additional packages such as @nxext/svelte, @angular-devkit/schematics, and @nrwl/react first. You can do that by running npm install @nxext/svelte @angular-devkit/schematics @nrwl/react --save-dev.

Running this command does 2 important things:

  1. It creates a core-components-react directory. This is where our React components are going to live.
  2. It adds the reactOutputTarget to the stencil.config.ts file in our Stencil project. Let’s have a look at it
reactOutputTarget({
      componentCorePackage: '@nx-stencil/core-components',
      proxiesFile:
        '../../../packages/core-components-react/src/generated/components.ts',
      includeDefineCustomElements: true,
    }),

At a high level, adding the reactOutputTarget to our Stencil config file indicates that we want to generate React components when we build our Stencil project. We specify that we want to put those components in our core-components-react directory with the proxiesFile option. You can read more about creating React wrapper components on the React integration page of the Stencil docs. Now, let’s go ahead and build our Stencil project to generate our React components.

nx build core-components

You will notice that running a build creates a generated folder in our React components library. This folder contains all of our React components and now they are ready to be used.

Using Our Components in a React App

Now that we have our React component library, we can start using those components in a React app. Let’s first create a new React app by running

nx generate @nrwl/react:application

You’ll be asked to provide a name and stylesheet format for your app, as well as whether or not you want to configure React router. I’m just going to call my app “react-app”.

Once the app is generated, we can go into the app.tsx file, remove the boilerplate code, and use our component.

import { MyComponent } from '@nx-stencil/core-components-react';

export function App() {
  return (
    <>
      <MyComponent first="First name" last="Last name" />
    </>
  );
}

export default App;

To run our app, we can run

nx serve react-app

NOTE: You may see an error indicating that members “must have an ‘override’ modifier”. You can resolve this by setting noImplicitOverride to false in the tsconfig.json file of the React app.

And we’ll see our component on the page!

Now let’s try doing the same thing for an Angular app.

Creating an Angular Component Library

The process of building an Angular component library from our Stencil components is very similar to the process we just went through, though there are some Angular-specific aspects we’ll have to pay particular attention to. Let’s start by creating our Angular component library with the Nx Stencil plugin.

nx g @nxext/stencil:add-outputtarget core-components --outputType='angular'

NOTE: You may receive an error indicating that you have to install @nrwl/angular first. You can do that by running npm install @nrwl/angular –save-dev.

Similar to the React command, this will create a core-components-angular directory to house our Angular components and it will also add the angularOutputTarget to our stencil.config.ts file.

angularOutputTarget({
      componentCorePackage: '@nx-stencil/core-components',
      directivesProxyFile:
        '../../../packages/core-components-angular/src/generated/directives/proxies.ts',
      // we won’t be using the ‘valueAccessorConfigs’, so feel free to remove this line
      valueAccessorConfigs: angularValueAccessorBindings,
    }),

With that in place, we can build our Stencil project to generate the Angular wrapped components.

nx build core-components

Once we’ve done that, and our Angular components have been generated, we need to declare and export our component from our module in core-components-angular.module.ts like so

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyComponent } from '../generated/directives/proxies';

@NgModule({
 imports: [CommonModule],
 declarations: [MyComponent],
 exports: [MyComponent]
})
export class CoreComponentsAngularModule {}

Using directivesArrayFile

Our configuration works just fine right now, but as we continue to add more components to our design system, we’ll have to continue to maintain the imports in core-components-angular.module.ts. As you might imagine, this will become increasingly cumbersome when we start to have dozens or even hundreds of components. Luckily, there’s an additional config option called directivesArrayFile that helps us manage these imports. Here’s how we can make use of it

angularOutputTarget({
      componentCorePackage: '@nx-stencil/core-components',
      directivesProxyFile:
      '../../../packages/core-components-angular/src/generated/directives/proxies.ts',
      directivesArrayFile:
      '../../../packages/core-components-angular/src/generated/directives/index.ts',
      valueAccessorConfigs: angularValueAccessorBindings,
    }),

With this config option, we now generate an index.ts file in the directives directory of our Angular component library. This file contains an array of all of our components. Now we can import this array in our Angular module, instead of each individual component.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DIRECTIVES } from '../generated/directives';


@NgModule({
  imports: [CommonModule],
  declarations: [...DIRECTIVES],
  exports: [...DIRECTIVES]
 })
 export class CoreComponentsAngularModule {}

With this approach, we’ll never need to touch this module again, even as we add more components to our Stencil project and generate new wrappers.

Using Our Components in an Angular App

Now our component is ready to be consumed by an Angular app; so, let’s create an Angular app to do exactly that. We can add an Angular app to our workspace by running

nx generate @nrwl/angular:application

Again, you’ll have to specify an app name, stylesheet format, and whether or not you want to configure routing. I’m going to call this app “angular-app”.

In the app.module.ts file of our new Angular app, we can import that CoreComponentsAngularModule from our component library.

import { CoreComponentsAngularModule } from '@nx-stencil/core-components-angular';

We’ll also need to import and call the defineCustomElements function from our Stencil project at the top of this file. The defineCustomElements function registers all of our components so they can be used in our Angular app.

import { defineCustomElements } from '@nx-stencil/core-components/loader';

defineCustomElements()

Finally, we need to add our CoreComponentsAngularModule to our module imports so we can make use of our components in our app component template.

@NgModule({
 declarations: [AppComponent, NxWelcomeComponent],
 imports: [BrowserModule, CoreComponentsAngularModule],
 providers: [],
 bootstrap: [AppComponent],
})
export class AppModule {}

And with that, we can use our my-component component in our app.component.html file.

<my-component first="First name" last="Last name"></my-component>

Let’s run nx serve angular-app and see our component in action!

This is just one way you can create and connect your Angular components to an Angular app. For more details on how to wrap and use your Stencil components in an Angular app, checkout the Stencil and Angular integrations docs.

Update on Save

Having all of these packages work together in our monorepo is great, but there’s one small improvement that we can make to improve the development experience. Right now, if we make a change to our Stencil component, the change is not reflected in our React or Angular app where the component is being used. This is because our applications are using the React and Angular wrapped versions of our component. If you recall, these wrapped components only update when we build our Stencil project. So to update our wrapped components, we can run

nx build core-components --watch

We use the --watch flag here because now our Stencil project will watch for any changes, and rebuild when they occur. That means the wrapped component libraries (React and Angular) will be updated, and Nx can then detect that dependency change in the React and Angular apps and update them accordingly. Now, any change to our Stencil component will propagate all the way through to our applications.

Conclusion

All in all, monorepos can be a really useful way to organize a project and Nx is a great tool to help you achieve a streamlined monorepo workflow. This tutorial highlights a lot of the core features that you may use when setting up a design system to support multiple frameworks, but there are plenty more tools and plugins to take your monorepo to the next level. Nx has tons of plugins including a plugin for Storybook, so you can document and test the components of your design system. If you’re interested in learning more about using Storybook in a Stencil project, checkout this guide on getting started with Stencil and Storybook. As always, I hope you found this tutorial helpful. Until next time, happy coding 😊.

Sign up for the Ionic Newsletter to get the latest news and updates!

Notable Replies

  1. Hi @pallard-src,

    This is a great question. To achieve the effect you’re describing, you’ll want to add your Stencil component library as an implicit dependency of your React wrapper component library. You can do this via the “implicitDependencies” option in the project.json file of your React wrapper component library. So, following the package names from the tutorial, it might look something like this "implicitDependencies": ["core-components"]. This will inform Nx that your Stencil library is a dependency of your React library and thus update your dependency graph accordingly.

    Here is Nx’s documentation on implicit dependencies: Configuration: project.json and nx.json | Nx

    I hope this is helpful. Feel free to let me know if you have any other questions.

Join the discussion on the Ionic Forum

5 more replies

Participants