I really like Solid JS, but...
...I can't live without libraries like react-three-fiber and react-flow. There are currently no alternative to those library in the Vanilla Javascript world. Solid is amazing: having both velocity (easy to code) and performance seems like a dream come true, but I can't leave React ecosystem (one of the largest ecosystem) behind. Writing all of the diffuse light and blur shadow in react-three-fiber, or recreate react-flow is insane. I'm not going to do that! I don't want to write 100.000 lines in Solid JS just to finally have to convert to React again!
That's why I write this article: how to integrate React component into Solid JS.
In part 1, we'll learn how to render a React component in Solid JS through simple minimal example like this:
You can check the source code here on Github.
In part 2, we'll learn how passing data between React component and Solid JS through this counter application:
You can check the source code here on Github.
In part 3 we'll learn how to build and config a more (somewhat) "real-world" application:
You can view this application here and check the source code here on Github.
There are many ways to integrate React component into Solid JS, here I'll introduce the 'monorepo' way of doing it.
Why it seems like an easy-to-solve problem but actually not
At first, integrate React into Solid JS seems like it's not going to be a problem at all. React and Solid is so similar. Can't we just install both React and Solid JS into one project, and then write parent component in Solid JS, the child component in React, and that's it?
Naive React component in Solid component implementation
It's not that simple.
Turns out the Vanilla JS that React code and Solid code are compiled down to are completely different (even the compiled JSX part is different!). The compiled React code is not "compatible" with the compiled Solid code. And we also have to tell Vite to compile React and Solid files differently.
Basically we have 2 big problems:
- How do we even tell Vite / CRA to compile React and Solid differently?
- How do React code and Solid code communicate with each other?
My approach to the compile problem is to create 2 different project, one for the React component and one for the Solid main app.
Solution to the compile problem: separate React and Solid project
For the communication problem my solution is to compile the React component into Vanilla JS and then install it into the main Solid app.
React and Solid can't understand each other but they both understand a common language: Vanilla JS. By compile the React component into Vanilla JS, Solid will be able to understand it.
Solution to the communication problem: compile React into Vanilla JS
Now that they understand each other, the Solid main application will give the React component a div to render on.
Here's a more technical diagram:
- The React component library expose a
mount
function: give it a div, themount
function will render on that div - Compile it into Vanilla JS
- Import it into the Solid application
- Since we compile the React code into Vanilla JS, the Solid application will understand
- Supply a div to the
mount
function and tell it to render
So let's get into how to make the React component into a separate library with the help of PNPM Workspace.
Solving the compile problem (with PNPM workspace)
We'll create 2 separate applications: React application and Solid application. Imagine the Solid application is the "main" application while the React application is a "library" that you npm install
into the Solid "main" application.
Here's the traditional workflow:
- Develop and build the React app
- Publish to NPM Registry
- Install it from NPM Registry to the Solid app
Looking at this workflow you might think, what's the point of making and publishing a library that only I use? I wish I don't have to publish to NPM Registry. Is there someway to use a package locally in my computer?
This is when PNPM workspace come to the rescue.
PNPM workspace
PNPM workspace helps one project use another project as a library without having to publish to NPM Registry.
Let's take an example: project #1 uses project #2 and project #3 as dependencies:
In our example, here's what PNPM Workspace would do:
- Project #1 asks PNPM Workspace for Project #2 and Project #3 as dependencies
- PNPM Workspace finds the location of Project #2 and Project #3 (through
pnpm-workspace.yaml
) - PNPM Workspace then asks for the build files of Project #2 and Project #3 (through
package.json
of each project) - PNPM Workspace deliver those files to Project #1
Here's a more... technical diagram explains the basic file structure of our example PNPM Workspace:
-
pnpm-workspace.yaml
at the root of project: tells pnpm workspace the location of all projects -
packages
is the folder houses all projects: if you rename this folder you have to change in thepnpm-workspace.yaml
accordingly -
package.json
in the "library" (in our example, project #2 and #3): show the entry to that library -
package.json
in "parent" project: the "workspace:*
" part tells pnpm workspace that it wants to use project #2 and project #3 locally
Yarn workspace and NPM workspace also offer the same functionality but in this article I'll use PNPM workspace. They're all really similar to each other.
So let's use PNPM workspace to build a Solid project that uses a React component.
Let's create a PNPM Workspace and add our Solid project to it
First, let's create a new folder for our new project. I'll name it react-in-solid
.
Next, create the pnpm-workspace.yaml
file tell PNPM about our main Solid project called "solid-project":
packages:
- "packages/solid-project"
Then, create the packages
folder that will house both our React and Solid project. In that folder, let's use Vite to create a new Solid project named "solid-project".
cd packages
npx create-vite@latest
√ Project name: ... solid-project
√ Select a framework: » Solid
√ Select a variant: » TypeScript
Here's a diagram explain what we did:
Now let's install dependencies. Remember to run install at the root folder. PNPM needs to know about pnpm-workspace.yaml
.
pnpm install
Next, let's run the server by:
pnpm run dev
Remember to run this in the packages/solid-project
folder!
Optional: the --filter
flag
Having to remember to switch folder to run certain command is quite annoying sometimes. So PNPM workspace offers the --filter
flag to solve this problem.
You can run the start dev server above command at the root project with the --filter
flag like this:
pnpm --filter \"solid-project\" run dev
The --filter
flag specify which project that the dev
command should run. It's like you run pnpm run dev
at the "solid-project" folder.
For our convenience, let's make a package.json
file at the root of the project and then add the script dev
. First, let's run:
pnpm init
Then, in the package.json
file add the "dev" script:
"scripts": {
"dev": "pnpm --filter \"solid-project\" run dev"
}
Now you can run pnpm run dev
in the root folder!
Let's create our React project and add to PNPM workspace
First, add our new project called react-component
into pnpm-workspace.yaml
to tell pnpm about it:
packages:
- "packages/solid-project"
- "packages/react-component"
Then, use Vite to create a new React project in the packages
folder.
cd packages
npx create-vite@latest
√ Project name: ... react-component
√ Select a framework: » React
√ Select a variant: » TypeScript + SWC
I use SWC here for speed but you can just choose Typescript and leave out SWC if you like
Next, install dependencies from the root folder:
pnpm install
Here's a diagram explain what we did:
Linking the React and Solid project together
Before continuing how to render a React component in a Solid project, you might want to get a rough idea about what Vite, Solid or React does.
Optional: what does Vite and React (and Solid) do?
In simple term, React (and Solid) allows us to "draw" our interface using Javascript on a div.
- You (the developers) write code to teach React (or Solid JS) how to create the interface we want
- We then supply an
index.html
with a root div - React draw on the root div
So with the bird's eye overview of what React does, here's what the setup code in the main.tsx
means:
We supply the createRoot
function with div with id "root", and then tell React (or Solid) to insert elements on that div based on the JSX code we write.
Since the browser only understand Vanilla JS, Vite then compile and bundle the React source code and our code into Vanilla JS for us, supply the index.html file with the "#root" div, and create the dev-server so that we can run in http://localhost:5173
.
Overall architecture
Back to our initial idea of how to render React component in a Solid app:
Combine with the basic understand of what React and Vite do for us behind the scene, here's the complete architecture:
- Our own React component library expose a
mount
function: give it a div, it'll render on that div - Compile our own component library into Vanilla JS
- Import it into the Solid application
- Supply a div to the
mount
function and tell it to draw - Vite will do the rest of the compiling, bundling, hot reloading,... for us
Let's apply it to build a Hello World "React in Solid application"!
Compile the React component (or we can call it make it a library!)
First, in the React app, let's delete everything in the ./src
directory. Then, create the App.tsx
file in that directory with the following content:
// src/App.tsx
export const App = () => {
return <div>Hi from React</div>;
};
The App component render the div with the text "Hi from React" inside.
Next, create the index.tsx
file with the following content:
// src/index.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
// For the main Solid app
export const mount = (root: HTMLElement) => {
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>
);
};
The mount
function accept a root HTML element and render the React element on it. The index.tsx
file serves as an entry point for the Solid app.
Now, let's compile the React code we write above to Vanilla JS for the main Solid app to use.
One of the most simple way to compile React TSX code into Vanilla JS is to use tsc
. tsc
(installed with Typescript) will compile .ts
, .tsx
file into Vanilla Javascript. You can use tools from Webpack (which CRA use), ESBuild (which Vite use), Rspack (which Rsbuild use), to tools that built specifically for compiling library like Tsup, Rslib, to very basic simple tool like tsc
, swc
. In this article for the sake of simplicity I'll use tsc
.
Let's create tsconfig.build.json
file specifically for tsc
to compile our React app. We'll tell tsc
to start with an entry file ./src/index.tsx
and output the compiled Vanilla JS into the dist
folder like so:
{
"compilerOptions": {
"target": "ES2016",
"jsx": "react-jsx",
"module": "Preserve",
"declaration": true,
"inlineSourceMap": true,
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"files": ["./src/index.tsx"]
}
Next, we'll tell tsc
to use the tsconfig.build.json
when compile our React app. In the package.json
of the react-component project, change the build command to this:
"scripts": {
...
"build": "tsc -p tsconfig.build.json",
...
},
Now, in the react-component project, let's run build our react component:
pnpm run build
You'll see it generate a new dist
folder with all the compiled Vanilla JS file with all the .d.ts
type file necessary for it.
The last step we need to do is to set the entry file for our React library. In this case, the entry is the compiled index.tsx
file: ./dist/index.js
. In the package.json
file, add to following:
{
"name": "react-component",
"main": "./dist/index.js",
...
}
Here's a diagram explain what we've done so far:
Optional: Running the react-component project independently
Out of all what Vite does for us, to make the React component a library we only need the compile phase.
This means that we can uninstall Vite and only keep the part that compile for us (which behind the scene is ESBuild / SWC). Though it's ok to do that, I want to keep Vite around so that we can have both options to run this project independently and serves as a library.
The tricky thing is while the main Solid application only expects the React component to return a mount
function but not to run it immediately, Vite expects the React component to run it immediately. You can see that in the default main.tsx
given to us will render our app in the #root
div immediately:
createRoot(document.getElementById('root')!).render(
<App/>,
)
To solve this problem, we'll create 2 entry file. The first is index.tsx
which export a mount
function for the main Solid application to use. The second is main.tsx
which actually run the mount
function on the #root
div.
Here's a more technical diagram:
Let's create the main.tsx
file that serves as an entry point for stand-alone development with the following:
// src/main.tsx
import { mount } from ".";
// For stand-alone development
mount(document.getElementById("root")!);
This main.tsx
file will immediately call the mount
function to render our React component in the #root
div.
In index.html
file of the React component application, make sure the tag import main.tsx
file:
...
<script type="module" src="/src/main.tsx"></script>
...
The index.html
file will import main.tsx
file (make sure it's not the index.tsx
file) which will run the React application for us.
If you run the server in react-component, you'll this on the screen:
Import into Solid application
First, let's "install" our React application as a dependency. In the package.json
of the solid-project, add the following:
"dependencies": {
...
"react-component": "workspace:*"
},
Here's a diagram explain what we've done so far:
Now let's run pnpm install
react component into the Solid main application. Remember to run at the root folder:
pnpm install
Next, in the solid-project app, let's delete everything in the src
folder. First, create the App.tsx
with the following content:
export const App = () => {
return <div>Hi from Solid</div>;
};
The App component render a div with text "Hi from Solid".
Next, create the index.tsx
with the following content:
import { render } from 'solid-js/web'
import { App } from './App.tsx'
const root = document.getElementById('root')
render(() => <App />, root!)
The index.tsx
render our app in the div with id "root".
If you run the server, you'll see the following in the screen:
Next, in App.tsx
, let's import the React application into our main Solid application:
import { mount } from "react-component";
import { onMount } from "solid-js";
export const App = () => {
let container!: HTMLDivElement;
onMount(() => {
mount(container);
});
return (
<>
<div ref={container} />
<div>Hi from Solid</div>
</>
);
};
The App.tsx
import mount
function from react component, and when the App component mount, give the mount
function a div.
Now, if you run the server, you'll see the following in the screen:
It's working! We have a React component rendered inside a Solid application.
That's it for part 1. In part 2 we'll solve to communication problem.
Credits
If you like the cute fish that I'm using, check out: https://thenounproject.com/browse/collection-icon/stripe-emotions-106667/.