DEV Community

Cover image for Integrate React component into Solid JS (with Typescript) (part 3)
TuanNQ
TuanNQ

Posted on

Integrate React component into Solid JS (with Typescript) (part 3)

In part 1 and part 2 we built a minimal application to explain how to render a React component inside Solid JS and how to communicate with each other. In this article we'll learn about configuration and create our own library through a "more real-world application".

Finish app screenshot

You can view it here and check the source code here on Github.

It's an application that render a 3D shirt with a configurations below to control lighting and camera,... You can also change the color of the shirt and upload logo. It's like the project from this video by Javascript Mastery but adding the configuration panel below the shirt.

Compare to previous simple application, this application is wayyy too big for me to walk line by line of code. Instead, I'll just focus on important part like configuration, mount function,... If you want to know more about React Three Fiber, you can check out this video on YouTube by JavaScript Master and this course by Anderson Mancini. To learn more about how to create beautiful settings in type of a graph that I did, check out https://reactflow.dev/.

Through this application, I'll explain some basic considerations and configurations you probably encounter when you create your own package in a monorepo project like:

  • entry file and bundling
  • .d.ts file
  • mount function improvements
  • ...and more

Why not use the tsc way?

You might think the tsc way I show you in part 1 is kind of... hacky. It's way too simple! There should be some standard or popular tool or workflow out there to help create a library.

Personally I think the tsc way it's fine. It really depends on the project. The simpler and few tools you use, the easier to understand and maintain. The more complex tools and workflows you use, the harder and more frustrated to config, understand and maintain.

But with that said, the simple tsc way has a few limitations:

  • What about CSS, Module CSS, TailwindCSS?
  • tsc only the know the React way of compiling JSX, so what if my library use Vue or Solid or Preact?
  • ...and more

To solve those limitations, there is a popular tool called tsup that specifically designed for building a library with a wide range articles and tutorials cover on how to use it.

But in this article I'll focus on how to do that using another powerful tool yet much less popular called Rslib.

Why Rslib?

First, because it's quite good! It's fast, relatively easy to config, and supports quite a lot of things out of the box. For example, since Rslib uses LightningCSS behind the scene, it has support for CSS, CSS Module and PostCSS without any further configuration. This means that to use TailwindCSS all you need to do is to install some Tailwind dependencies and create postcss.config.ts file with:

export default { plugins: { "@tailwindcss/postcss": {} } };
Enter fullscreen mode Exit fullscreen mode

Plus Rslib is really fast and scalable for large project since it uses Rspack under the hood.

The second reason I choose to write about Rslib is that it was released recently (about August 2024 or so), so currently there are not a lot of tutorials out there about it yet.

Setup our application

Our application consists of 3 small applications:

  • react-shirt: render the 3D shirt
  • react-flow: render the camera, lighting,... configuration below the shirt
  • solid-project: main application, render the sidebar, react-shirt and react-flow

ThreeJS Shirt App explain

User can change the intensity of ambient and randomize light, frames of accumulate shadow, and the Z axis of camera in the settings in react-flow subproject. When the settings is changed, the configuration data will flow from react-flow to solid-project to react-shirt.

The main app solid-project will hold any state that necessary for the whole application.

Here's the basic file structure of the project:

Project structure

Let's talk a little more about how I did the setup part.

First, I created a new folder called threejs-shirt-solid-in-react.

Next, to create package.json file at the root project, I ran:

pnpm init
Enter fullscreen mode Exit fullscreen mode

Next I created pnpm-workspace.yaml file with the following content:

packages:
  - "packages/solid-project"
  - "packages/react-shirt"
  - "packages/react-flow"
Enter fullscreen mode Exit fullscreen mode

Next, I created a new folder called packages to hold 3 applications: solid-project, react-shirt, and react-flow.

Now let's talk little more about react-flow.

It receives initial data configuration like ambientLightIntensity, randomizeLightIntensity,... from solid-project and communicate changes back to solid-project through callbacks like onAmbientLightingIntensityChange, onRandomizeLightIntensityChange,...

Data flow diagram

Let's create this library react-flow using Rslib. In the packages folder, run:

pnpm create rslib@latest

◆  Create Rslib Project
│
◇  Project name or path
│  react-flow
│
◇  Select template
│  React
│
◇  Select language
│  TypeScript
│
◇  Select development tools (Use <space> to select, <enter> to continue)
│  none
│
◇  Select additional tools (Use <space> to select, <enter> to continue)
│  none
Enter fullscreen mode Exit fullscreen mode

The configuration above will generate a src folder with index.tsx, Button.tsx and button.css as an example, a .gitignore file for git, a package.json file, a README.md file, a tsconfig.json for Typescript configuration, and a rslib.config.ts as a configuration on how to build a library.

Init project with Rslib screenshot

Understand Rslib configuration

The default rslib.config.ts for creating a React component library look like this:

import { pluginReact } from '@rsbuild/plugin-react';
import { defineConfig } from '@rslib/core';

export default defineConfig({
  source: {
    entry: {
      index: ['./src/**'],
    },
  },
  lib: [
    {
      bundle: false,
      dts: true,
      format: 'esm',
    },
  ],
  output: {
    target: 'web',
  },
  plugins: [pluginReact()],
});
Enter fullscreen mode Exit fullscreen mode

Let's break down what this config file do for us.

Bundle and entry settings

First let's talk about what source.entry.index = ['./src/**'] means.

source: {
  entry: {
    index: ['./src/**'],
  },
},
Enter fullscreen mode Exit fullscreen mode

source.entry.index = ['./src/**'] means that Rslib treat every files in the /src folder as an entry file to compile. If you config like this:

source: {
  entry: {
    index: ['./src/index.tsx'],
  },
},
Enter fullscreen mode Exit fullscreen mode

...Rslib will only treat index.tsx as an entry file and ignore everything else.

Rslib entry file setting explain

Next is the lib.bundle part:

lib: [
    {
      bundle: false,
      ...
    },
  ],
Enter fullscreen mode Exit fullscreen mode

This piece of configuration means Rslib won't bundle entry file and all of its dependencies into one file for us.

For example, let's say you have an index.tsx file that import Label.tsx and Button.tsx file. If you set bundle: false, the output (what you see in the dist folder) will be 3 files: index.js, label.js and button.js. If you set bundle: true, everything will be bundle up into one file index.js.

Rslib bundle option explain

Now let's combine these 2 settings together:

source: {
  entry: {
      index: ['./src/**'],
    },
},
lib: [
  {
    bundle: false,
    ...
  },
]
Enter fullscreen mode Exit fullscreen mode

This means that Rslib will treat every files in the src folder as an entry file, compile all of it, and not bundle all of them together. Now if you change the source.entry.index = ['./src/index.tsx'] but keep the lib.bundle = false like this:

source: {
  entry: {
      index: ['./src/index.tsx'],
    },
},
lib: [
  {
    bundle: false,
    ...
  },
]
Enter fullscreen mode Exit fullscreen mode

...the result would be fatal. This setting means that Rslib will only compile index.tsx but none of the files that its import.

Rslib bundle entry settings compare

So when do we want to bundle everything into one file and when do we want the bundle-less option?

For example let's say you build a React library component. You have all kinds of components Button, Label, Input, Card,... but the user only use the Button component. If you bundle everything together the user will have not choice but to include everything despite he only use the Button component. If you use the bundle-less option the user might be able to just include the Button.tsx file with all its dependency instead of having to import everything. This is called "tree-shaking".

dts settings

Next is the lib.dts = true settings. One project knows which type of another project's components, which props does it takes, which output does it returns through the .d.ts file.

dts file explanation

For example, if I have a Card component like this:

interface Props {
  header?: React.ReactNode;
  body?: React.ReactNode;
}

export const Card: React.FC<Props> = (props) => {
    ...
};
Enter fullscreen mode Exit fullscreen mode

Here's the corresponding .d.ts file:

interface Props {
    header?: React.ReactNode;
    body?: React.ReactNode;
}

export declare const Card: React.FC<Props>;
Enter fullscreen mode Exit fullscreen mode

Now if another project use my Card component, it'll know my Card take 2 optional props header and body that is a ReactNode.

So the setting lib.dts = true:

  lib: [
    {
      ...
      dts: true,
    },
  ],
Enter fullscreen mode Exit fullscreen mode

...means that Rslib will generate corresponding .d.ts files for each of the .tsx, .ts files for us.

Format esm setting

Do you remember there's 2 ways to import another Javascript file? One is require and the other is import.

require is the old CommonJS way that NodeJS uses. import is the newer ESM way that modern browsers use. ESM provide numerous benefit, one of them is to allow tree shaking I talk about earlier. Because of that, we'll use the ESM way:

lib: [
  {
    ...
    format: 'esm',
  },
],
Enter fullscreen mode Exit fullscreen mode

Output target web setting

Next is the output.target = web setting.

output: {
  target: 'web',
}
Enter fullscreen mode Exit fullscreen mode

To build an application for the browser to use, we need to solve a lot of problems. One example is the CSS problem. A library built for NodeJS does not need to deal with CSS, SCSS, CSS Module, PostCSS, minimize and transform CSS,... but a library built for web does.

output.target = web means that Rslib will solve many problems related to build for web for us.

Plugin React setting

Finally is the plugin setting:

{
  ...
  plugins: [pluginReact()],
}
Enter fullscreen mode Exit fullscreen mode

One of the primary job of a library builder tool like Rslib is to compile framework-specific language (like JSX) into the good old Vanilla Javascript. The pluginReact() plugin will help Rslib to do that.

For example if you use another framework like SolidJS, here's the configuration:

plugins: [
  pluginBabel({
    include: /\.(?:jsx|tsx)$/,
  }),
  pluginSolid(),
],
Enter fullscreen mode Exit fullscreen mode

Optional reading here: why the Solid configuration need the pluginBabel?

Babel and SWC essentially do the same job: compile ts, tsx, jsx,... and many framework specific things into Vanilla Javascript. Say if you want to create your own language or framework and then compile to Vanilla Javascript, you can teach Babel and SWC to do so by writing your own plugin.

Behind the scene Rslib use SWC by default since it's the fastest. But currently since the Solid framework author only create plugin for Babel but not for SWC, we need to tell Rslib explicitly to install and use Babel and then use the Solid plugin for Babel to compile.

Use Tailwind CSS with Rslib

Tailwind CSS uses PostCSS to look at what classes do you use and add those classes into css file. Since behind the scene Rslib uses LightningCSS which supports PostCSS by default (like Vite also supports PostCSS by default), we only need to install a few TailwindCSS dependencies and add necessary postcss configuration to use it.

First, let's install necessary TailwindCSS dependencies:

pnpm install @tailwindcss/postcss tailwindcss
Enter fullscreen mode Exit fullscreen mode

Next, create postcss.config.ts file with the following content:

export default { plugins: { "@tailwindcss/postcss": {} } };
Enter fullscreen mode Exit fullscreen mode

In the main css file add the following according to the Tailwind documentation:

@import "tailwindcss";
Enter fullscreen mode Exit fullscreen mode

That's it! Now we're ready to use Tailwind in our project.

Optional: dts settings abort on error

The default setting lib.dts = true means that Rslib will generate .d.ts files for us. But it also means that if any type error happens, Rslib will also stop compiling.

For example if I tell Rslib to compile the following code:

interface Options {
    fullscreen?: boolean;
}

const options: Options = {
    fullScreen: true,
    hiThere: false // <-- type error here: 'hithere' does not exist in type 'Options'
}
Enter fullscreen mode Exit fullscreen mode

Despite the this code can be compiled to Javascript and can be execute without any error, the type error will stop Rslib from compiling.

I think this behavior is quite annoying when coding. When I'm coding, I want to experiment with things and see changes fast instead of doing everything right on the start.

So I change the dts config from lib.dts = true to:

  lib: [
    {
      ...
      dts: {
        abortOnError: false,
      },
    },
  ],
Enter fullscreen mode Exit fullscreen mode

This means that if type errors occur, Rslib will still compile everything for us.

Serves both as a library and running independently

In part 1 we use tsc to compile our library into Vanilla Javascript and Vite to run the React component independently. We create 2 entry files index.tsx for tsc and main.tsx for Vite.

old architecture

In this part we'll use Rslib to compile and its cousin Rsbuild to run it independently. (Rslib and Rsbuild come from the same author, Rslib actually uses Rsbuild and both of them are extremely fast for large project!)

New architecture using Rslib and Rsbuild

Add Rsbuild

First, let's install Rsbuild. In the react-flow folder, run:

pnpm install @rsbuild/core
Enter fullscreen mode Exit fullscreen mode

Next, let's tell Rsbuild to use main.tsx as an entry file by create rsbuild.config.ts with the following content:

import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";

export default defineConfig({
  plugins: [pluginReact()],
  source: {
    entry: {
      index: "./src/main.tsx",
    },
  }
});
Enter fullscreen mode Exit fullscreen mode

Optional, let's tell Rsbuild to use dist-dev as a folder to output the build result to instead of the default dist:

import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";

export default defineConfig({
  plugins: [pluginReact()],
  source: {
    entry: {
      index: "./src/main.tsx",
    },
  },
  output: {
    distPath: {
      root: "dist-dev",
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

I like to add this configuration because each time Rsbuild run, it'll clear all the dist folder by default. In our situation, since both Rslib and Rsbuild use the same dist folder, it's quite annoying: if you run Rslib and Rsbuild at the same time, Rsbuild will clear all the output of Rslib.

Config Rslib to ignore the main.tsx file

Next, let's tell Rslib to ignore the main.tsx file:

import { pluginReact } from "@rsbuild/plugin-react";
import { defineConfig } from "@rslib/core";

export default defineConfig({
    source: {
        entry: {
            index: ["./src/**"],
        },
        exclude: ["./src/main.tsx"],
    },
    ...
})
Enter fullscreen mode Exit fullscreen mode

The main.tsx file is only needed for Rsbuild to render independently. We don't want to include it to the compiled result.

Mount function improvements

Render many times

The mount function can be called many times due to the main application re-rendering. So let's unmount all the previous root and re-mount to the newly provided root div:

let root: ReactDOM.Root | undefined = undefined;

export const mount = (rootEl?: Element) => {
    // If not provided with a component to render, return
    if (!rootEl) {
        return;
    }

    // If already render before, unmount it
    if (root) {
        root.unmount();
    }

    // Render on root element
    root = ReactDOM.createRoot(rootEl);
    root.render(
        <React.StrictMode>
            <App />
        </React.StrictMode>,
    );
};
Enter fullscreen mode Exit fullscreen mode

Render on full screen option

In the mount function of react-flow component, we use react-flow to render on the provided div. It is up to main Solid application to specify the width and height of that div. But when we render it independently, the outer div is the #root div with no width and height.

If you try to run it independently, you'll see a blank screen with the following error in the console: [React Flow]: The React Flow parent container needs a width and a height to render the graph. like this.

Error screen snapshot

So let's add an option to the mount function to render on full screen if needed. In the index.tsx change to the following:

// Options (fullscreen: for running this app independently)
interface Options {
  fullscreen?: boolean;
}

// Mount
let root: ReactDOM.Root | undefined = undefined;

export const mount = (
  rootEl: Element | null | undefined,
  props?: Callbacks & InitialValue,
  options?: Options,
) => {
  // If not provided with a component to render, return
  if (!rootEl) {
    return;
  }

  // If already render before, unmount it
  if (root) {
    root.unmount();
  }

  // Options
  let AppWrapper: React.FC = App;

  if (options?.fullscreen) {
    AppWrapper = () => (
      <div className="w-screen h-screen">
        <App />
      </div>
    );
  }

  // Render on root element
  root = ReactDOM.createRoot(rootEl);

  root.render(
    <React.StrictMode>
      <AppWrapper />
    </React.StrictMode>,
  );
};
Enter fullscreen mode Exit fullscreen mode

Then, in the main.tsx we can add the full screen option:

import { mount } from ".";

mount(document.querySelector("#root"), undefined, {
    fullscreen: true,
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

So that's all important details and configurations I think it'll be helpful to you when building your own library.

I hope with this 3 parts tutorial and my sample project in Github, you can leverage the blazingly fast and ease-of-use of Solid with the of wide ecosystem of React in your project.

I hope this article is helpful to you. If you have any improvements or other approaches to the integrate React into Solid problem, please let me know below.

Top comments (0)