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".
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": {} } };
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
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:
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
Next I created pnpm-workspace.yaml
file with the following content:
packages:
- "packages/solid-project"
- "packages/react-shirt"
- "packages/react-flow"
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
,...
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
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.
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()],
});
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/**'],
},
},
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'],
},
},
...Rslib will only treat index.tsx
as an entry file and ignore everything else.
Next is the lib.bundle
part:
lib: [
{
bundle: false,
...
},
],
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
.
Now let's combine these 2 settings together:
source: {
entry: {
index: ['./src/**'],
},
},
lib: [
{
bundle: false,
...
},
]
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,
...
},
]
...the result would be fatal. This setting means that Rslib will only compile index.tsx
but none of the files that its import.
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.
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) => {
...
};
Here's the corresponding .d.ts
file:
interface Props {
header?: React.ReactNode;
body?: React.ReactNode;
}
export declare const Card: React.FC<Props>;
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,
},
],
...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',
},
],
Output target web setting
Next is the output.target = web
setting.
output: {
target: 'web',
}
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()],
}
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(),
],
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
Next, create postcss.config.ts
file with the following content:
export default { plugins: { "@tailwindcss/postcss": {} } };
In the main css file add the following according to the Tailwind documentation:
@import "tailwindcss";
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'
}
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,
},
},
],
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.
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!)
Add Rsbuild
First, let's install Rsbuild. In the react-flow
folder, run:
pnpm install @rsbuild/core
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",
},
}
});
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",
},
},
});
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"],
},
...
})
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>,
);
};
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.
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>,
);
};
Then, in the main.tsx
we can add the full screen option:
import { mount } from ".";
mount(document.querySelector("#root"), undefined, {
fullscreen: true,
});
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)