Mình rất thích Solid, nhưng...
...mình không thể sống thiếu react-three-fiber và react-flow. Hiện tại không có thư viện nào có khả năng thay thế nó cả.
Solid thật tuyệt vời, nếu bạn đã từng code React rồi thì Solid không những dễ dàng và quen thuộc, mà còn vừa chạy rất nhanh. Tuy nhiên mình không thể rời bỏ React ecosystem (một trong những ecosystem lớn nhất) được. Code lại tất cả những thứ như diffuse light và blur shadow trong react-three-fiber, hay code lại hoàn toàn cả react-flow là không thể chấp nhận được. Mình không muốn viết 10.000 dòng bằng Solid JS chỉ để phải chuyển lại về React!
Vì thế mình viết bài viết này: làm sao để integrate React component vào Solid JS.
Ở phần 1, mình sẽ giải thích làm sao để render một component React ở trong Solid JS:
Bạn có thể xem source code cho phần 1 ở đây.
Ở phần 2, mình sẽ giải thích làm thế nào để "kết nối" (chuyển data) giữa component React và Solid JS:
Bạn có thể xem source code cho phần 2 ở đây.
Ở phần 3 mình sẽ giải thích những config phổ biến bạn nên biết bằng một ứng dụng phức tạp hơn một chút:
Bạn có thể xem ứng dụng này tại đây và check source code tại đây trên Github.
Trong khi có rất nhiều cách để integrate một component React vào trong Solid JS, ở bài viết này mình sẽ hướng dẫn bạn làm điều đó theo cách "monorepo".
Tại sao integrate một component React vào Solid JS không đơn giản chút nào?
Mới đầu mình nghĩ để integrate một component React vào một project dùng Solid JS rất đơn giản. Solid JS và React rất giống nhau. Như vậy thì chắc chỉ cần install Solid và React vào cùng một project, viết component bình thường bằng Solid JS, rồi component con bằng React là được.
Giải pháp đơn giản mới đầu mình nghĩ
Nhưng buồn thay nó không đơn giản như thế.
Khi compile React và Solid ra Javascript thuần, code Javascript được compile này không giống nhau và không pass props được cho nhau một chút nào (kể cả phần JSX cũng khác nhau hoàn toàn!). Hơn nữa, mình cũng phải tìm cách bảo Vite (hay CRA, NextJS,...) compile những file được viết bằng React và Solid một cách khác nhau.
Cơ bản mình có 2 vấn đề chính:
- Làm sao để bảo Vite / CRA compile những file được viết bằng React hay Solid một cách khác nhau.
- Làm sao để những component được viết bằng Solid và React giao tiếp được với nhau.
Giải pháp của mình cho vấn đề compile là tạo ra 2 project: một cái cho component React và cái còn lại cho project chính bằng Solid JS.
Giải pháp cho vấn đề compile: 2 project React và Solid riêng biệt
Giải pháp cho vấn đề giao tiếp với nhau là compile component React thành Javascript thuần rồi install nó như một thư viện vào project Solid chính.
React và Solid không thể giao tiếp trực tiếp được với nhau nhưng cả hai đều "hiểu" Javascript thuần. Bằng cách compile component React thành Javascript thuần, Solid sẽ "hiểu" component React này.
Giải pháp cho vấn đề giao tiếp: compile componet React thành Javascript thuần
Bây giờ sau khi project Solid JS chính đã "hiểu" component React này, tiếp theo project Solid JS chính này sẽ tạo ra một cái <div/>
rồi đưa cho component React để render trên nó.
Đây là hình vẽ minh họa giải thích cả quá trình nhìn trông "chuyên nghiệp" hơn một chút:
- Project riêng của component React sẽ export ra function
mount
nhận vào một cái<div/>
- Compile project này thành Javascript thuần (hay còn gọi là quá trình "build")
- Install nó vào project Solid JS
- Vì mình đã compile project component React này thành Javascript thuần, khi install vào trong project Solid JS, project Solid JS sẽ "hiểu" component React này
- Truyền một cái
<div/>
vào functionmount
Vậy hãy bắt đầu bằng việc tạo 2 project riêng biệt rồi ghép chúng vào với nhau bằng PNPM Workspace.
Giải quyết vấn đề compile (bằng PNPM Workspace)
Mình sẽ tạo 2 project: một project cho component React và một project chính bằng Solid JS. Tưởng tượng project component React này giống như một "thư viện" mà mình npm install
vào project Solid JS chính.
Như vậy thì ngày xưa mình sẽ phải làm thế này:
- Build project component React như một thư viện
- Publish nó lên NPM Registry
- Install nó về từ NPM Registry vào Solid app
Nhìn vào hình trên bạn có thể nghĩ, việc phải publish project component React của mình lên NPM Registry thành một thư viện thật... vô nghĩa. Sau cùng thì cũng chỉ có mỗi mình mình dùng "thư viện" đó của mình chứ có ai dùng nữa đâu. Giá mà có cách nào để không phải publish lên NPM Registry mà vẫn "install" được vào project Solid chính của mình thì tiện nhỉ.
PNPM Workspace được sinh ra để giải quyết vấn đề này.
PNPM workspace
PNPM workspace giúp một project sử dụng một project khác như một "thư viện" mà không cần phải publish lên NPM Registry.
Để giải thích một cách dễ hiểu hơn hãy lấy một ví dụ: project #1 dùng project #2 và project #3:
Ở ví dụ này:
- Project #1 sẽ "hỏi" PNPM Workspace để install Project #2 và Project #3
- PNPM Workspace sẽ tìm vị trí của Project #2 và Project #3 (bằng file
pnpm-workspace.yaml
) - PNPM Workspace tiếp theo sẽ tìm những file được build ra của Project #2 và Project #3 (bằng file
package.json
của mỗi project) - PNPM Workspace chuyển những file này cho Project #1
Đây là hình vẽ minh họa giải thích về cấu trúc thư mục cơ bản của một project dùng PNPM Workspace:
pnpm-workspace.yaml
ở thư mục ngoài cùng của project tổng: để pnpm workspace biết tất cả vị trí của "project con"packages
là thư mục chứa tất cả project con: nếu bạn đổi tên thư mục này bạn sẽ phải sửa tương ứng trongpnpm-workspace.yaml
package.json
của các project đóng vai trò là "thư viện" (như trong ví dụ của mình sẽ là project #2 và #3): chỉ vị trí những file được "build" của project đópackage.json
ở project "chính": chữ "workspace:*
" nói cho pnpm workspace biết rằng project này muốn sử dụng project #2 và project #3 đã có ở trên máy mình (chứ không phải download về từ NPM Registry)
Giống như PNPM Workspace, Yarn workspace và NPM workspace cũng được sinh ra để giải quyết những vấn đề này và thực sự cũng rất giống nhau. Ở bài viết này mình sẽ sử dụng PNPM Workspace.
Vậy sau khi hiểu "hòm hòm" về PNPM Workspace làm gì và như thế nào, ở phần tiếp theo hãy cùng tạo ra một project tổng dùng PNPM Workspace có 2 project con:
- Project component React.
- Project Solid JS chính dùng project component React trên
Tạo project tổng dùng PNPM Workspace và thêm project Solid JS vào
PNPM Workspace được "đính kèm" trong pnpm. Nếu máy bạn đã có pnpm thì bạn có thể dùng pnpm workspace. Nếu bạn chưa cài đặt pnpm trong máy thì bạn chỉ cần:
npm i -g pnpm
...là được.
Tiếp theo, hãy tạo một thư mục mới. Mình sẽ đặt tên thư mục của mình là react-in-solid
.
Tiếp theo, tạo file pnpm-workspace.yaml
để nói cho PNPM biết vị trí của project Solid của mình (mình sẽ đặt tên project này là "solid-project"):
packages: - "packages/solid-project"
Tiếp theo, mình sẽ tạo thư mục packages
để chứa project Solid. Ở trong thư mục, mình sẽ sử dụng Vite để tạo ra một project Solid mới có tên là "solid-project".
cd packages npx create-vite@latest √ Project name: ... solid-project
√ Select a framework: » Solid
√ Select a variant: » TypeScript
Đây là hình vẽ minh họa giải thích những gì mình vừa làm:
Tiếp đến hãy install dependencies bằng pnpm install
. Nhớ rằng hãy chạy lệnh này ở thư mục gốc. Vì file pnpm-workspace.yaml
nằm ở thư mục gốc, chỉ có như vậy thì pnpm mới biết về file này.
pnpm install
Tiếp theo, hãy chạy server dev bằng:
pnpm run dev
Hãy nhớ chạy lệnh này ở trong thư mục packages/solid-project
!
Optional: flag --filter
Phải nhớ chuyển folder khi chạy mỗi lệnh khác nhau khá phiền phức. Vì vậy, PNPM workspace có flag --filter
để giải quyết vấn đề này.
Bạn có thể chạy lệnh start server dev ở trên ở thư mục gốc với flag --filter
như thế này:
pnpm --filter \"solid-project\" run dev
Flag --filter
này chỉ ra lệnh dev
này chạy ở project nào. Nó giống như bạn chạy lệnh pnpm run dev
ở thư mục "solid-project" vậy.
Để cho tiện, hãy tạo file package.json
ở thư mục gốc và thêm script dev
để chạy server dev ở project "solid-project" . Đầu tiên, hãy chạy:
pnpm init
Tiếp theo, ở file package.json
hãy thêm script `dev:
"scripts": { "dev": "pnpm --filter \"solid-project\" run dev"
}
Bây giờ bạn có thể chạy pnpm run dev
ở thư mục gốc rồi!
Tạo project component React và thêm nó vào PNPM workspace
Đầu tiên, hãy thông báo cho pnpm workspace biết mình có một project mới tên là react-component
bằng cách thêm vào file pnpm-workspace.yaml
như sau:
packages: - "packages/solid-project" - "packages/react-component"
Tiếp theo, mình sẽ dùng Vite để tạo ra project component React ở thư mục packages
.
cd packages npx create-vite@latest √ Project name: ... react-component
√ Select a framework: » React
√ Select a variant: » TypeScript + SWC
Mình sử dụng SWC ở đây vì SWC rất nhanh nhưng bạn có thể chỉ sử dụng Typescript mà không dùng SWC cũng được
Tiếp theo, hãy install dependencies ở thư mục gốc bằng:
pnpm install
Đây là hình vẽ minh họa giải thích những gì mình vừa làm:
Nối 2 project React and Solid vào với nhau
Trước khi tiếp tục làm sao để render component React ở trong Solid, mình sẽ giải thích một cách sơ qua ngắn gọn Solid, React, Vite làm gì.
Optional: Vite và React (và Solid) làm gì?
Một cách đơn giản, React (hay Solid) cho phép mình "vẽ" giao diện mình muốn bằng Javascript trên một cái div.
- Developer viết code để "dạy" React (hay Solid JS) làm sao để tạo ra giao diện mà mình muốn
- Sau đó mình có file
index.html
với một thẻdiv
đưa cho React - React sẽ "vẽ" giao diện mình muốn trên thẻ
div
đó
Vậy với cách hiểu đơn giản như thế, đoạn code setup ở trong file main.tsx
mà bạn nhìn thấy nghĩa là thế này:
Mình truyền cho function createRoot
một cái div có id là "root" (div này phải tồn tại trong file index.html
), và bảo nó render trên cái div đó giao diện mà mình muốn.
Vì trình duyệt chỉ hiểu Javascript thuần (chứ không hiểu Typescript với JSX), Vite sẽ compile và bundle code của thư viện React với code mình viết thành Javascript thuần, rồi cùng với đó đưa cho server dev file index.html
với div có id là "root" để mình có thể truy cập vào ở http://localhost:5173
.
Kiến trúc tổng thể của cả project
Nhắc lại một chút về ý tưởng làm sao để render một component React vào Solid mà mình đã nói ở trên:
...cùng với những gì mà Vite đã làm cho mình, đây là kiến trúc tổng quan của cả project:
- Project component React của mình sẽ export function
mount
nhận vào thẻ div để render component React trên đó - Compile project này về Javascript thuần
- Import nó vào project Solid
- Project Solid sẽ đưa function
mount
một cáidiv
để nó render trên đấy - Vite sẽ hoàn thành nốt phần việc còn lại: compiling, bundling,... và mình có thể vào
http://localhost:5173
để xem
Bây giờ sau khi đã hiểu về kiến trúc tổng quan dự án, hãy qua phần tiếp theo bắt tay vào render component React ở trong Solid.
Compile project component React về Javascript thuần (hay còn gọi là build)
Đầu tiên, ở project component React, hãy xóa tất cả những file trong thư mục ./src
. Tiếp theo, tạo file App.tsx
ở trong như mục đó như thế này:
// src/App.tsx
export const App = () => { return <div>Hi from React</div>;
};
Ở đây mình tạo một component mà sẽ render một div với text là "Hi from React".
Tiếp theo tạo file index.tsx
như thế này:
// 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> );
};
Function mount
nhận vào một div và sẽ render app của mình trên đó. File index.tsx
đóng vai trò là "entry file" cho project Solid.
Bây giờ, mình sẽ compile code mà mình viết thành Javascript thuần để import vào project Solid JS.
Một trong những cách đơn giản nhất để compile code mà mình viết (React TSX) thành Javascript thuần là dùng tsc
. tsc
(thư viện đính kèm với Typescript khi bạn install Typescript) sẽ compile những file .ts
, .tsx
thành file .js
Javascript thuần. Bạn có thể dùng từ những công cụ như Webpack (cái mà CRA dùng), ESBuild (cái mà Vite dùng), Rspack (cái mà Rsbuild dùng), cho đến những công cụ được tạo ra dành riêng cho việc tự build một thư viện như Tsup, Rslib, cho đến những công cụ đơn giản nhất như tsc
, swc
. Ở trong bài viết này để giữ mọi thứ tối giản nhất có thể mình sẽ sử dụng tsc
.
Bây giờ mình sẽ tạo ra file tsconfig.build.json
:config dành cho tsc
chỉ dùng cho lúc compile project React component. Mình sẽ bảo tsc
bắt đầu compile với entry file là ./src/index.tsx
rồi đưa ra kết quả trả về vào thư mục dist
như thế này:
{ "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"]
}
Tiếp theo, mình sẽ bảo tsc
dùng file tsconfig.build.json
này để build app của mình (hay còn gọi là compile code của mình thành Javascript thuần). Ở file package.json
ở project component React, mình sẽ đổi script build
thành:
"scripts": { ... "build": "tsc -p tsconfig.build.json", ... },
Bây giờ, hãy build project component React của mình bằng cách chạy lệnh build:
pnpm run build
Bước cuối cùng mình cần làm là thông báo cho pnpm biết làm sao để tìm những file được build của mình. Ở trong file package.json
, thêm trường main
vào như sau:
{ "name": "react-component", "main": "./dist/index.js", ...
}
Đây là sơ đồ giải thích những gì mình làm từ trước đến giờ:
Optional: chạy project react-component một cách độc lập
Trong tất cả những gì Vite làm cho mình, mình chỉ cần phần compile code của mình thành Javascript thuần (Vanilla Javascript) như mình đã khoanh ở đây.
Điều này nghĩa là mình có thể uninstall Vite và chỉ giữ cái phần mà compile code của mình về Javascript thuần (ESBuild / SWC). Mặc dù hoàn toàn có thể làm như thế, mình muốn giữ Vite lại để mình vẫn có thể chạy project component React này một cách độc lập.
Tuy nhiên cái khó ở đây là trong khi project Solid chính chỉ muốn function mount
, Vite lại muốn mình chạy function mount
đó ngay lập tức với #root
div. Bạn có thể thấy bình thường ở file main.tsx
lập tức render ứng dụng của mình trên #root
div như thế này:
createRoot(document.getElementById('root')!).render( <App/>,
)
Để giải quyết vấn đề này, mình sẽ tạo ra 2 "entry file" khác nhau. Entry file đầu tiên là index.tsx
sẽ export ra function mount
để project Solid sử dụng. Entry file thứ hai là main.tsx
sẽ chạy function mount
ngay lập tức với #root
div.
Đây là hình vẽ minh họa "kỹ thuật" hơn một chút:
Bây giờ hãy tạo file main.tsx
như thế này:
// src/main.tsx
import { mount } from "."; // For stand-alone development
mount(document.getElementById("root")!);
Ở file index.html
của project component React, hãy đảm bảo rằng tag <script/> import file main.tsx
như thế này:
... <script type="module" src="/src/main.tsx"></script>
...
File index.html
file sẽ import file main.tsx
(hãy chắc chắn rằng nó không phải file index.tsx
) là entry file để render component React của mình.
Nếu bạn chạy server dev của project react-component
rồi truy cập vào http://localhost:5173
, bạn sẽ thấy thế này:
Import vào project Solid chính
Đầu tiên, hãy "install" thư việc react-component của mình giống như một "package" hay "dependency" bằng cách thêm vào file package.json
của project Solid của mình như sau:
"dependencies": { ... "react-component": "workspace:*" },
Đây là sơ đồ giải thích những gì mình vừa làm:
Bây giờ hãy chạy pnpm install
để install react-component vào solid-project. Hãy nhớ chạy lệnh này ở thư mục gốc:
pnpm install
Bây giờ, ở project chính Solid JS, hãy xóa tất cả những file trong thư mục src
. Tiếp đến, hãy tạo ra file App.tsx
với nội dung như sau:
export const App = () => { return <div>Hi from Solid</div>;
};
Component App render một cái div với text bên trong là "Hi from Solid".
Tiếp theo, tạo file index.tsx
với nội dung như sau:
import { render } from 'solid-js/web'
import App from './App.tsx' const root = document.getElementById('root') render(() => <App />, root!)
File index.tsx
sẽ render app của mình trên một cái div với id là "root".
Nếu bạn chạy server pnpm run dev
và vào http://localhost:5173
, bạn sẽ thấy thế này:
Tiếp theo, trong file App.tsx
, hãy render component React của mình:
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> </> );
};
Ở trong file App.tsx
sẽ import function mount
của project component React, và khi component Solid của mình khởi chạy (onMount
), mình sẽ chạy function mount
với div container.
Bây giờ nếu bạn truy cập http://localhost:5173
, bạn sẽ thấy thế này trên màn hình:
Thật tuyệt vời! Và đó là cách mình render một component React trong một project dùng Solid JS.
Ở phần 2 mình sẽ tiếp tục giải quyết vấn đề làm sao để "giao tiếp" (chuyển data) giữa component React và app dùng Solid chính của mình.