Lại một JS framework nữa? Tại sao mình lại nên quan tâm đến framework Solid JS này? Không phải React đã đủ tốt để dùng sao? Dưới đây là những lý do tại sao bạn nên quan tâm đến Solid JS nếu là một lập trình viên React.
Đã bao giờ bạn gặp một component React khổng lồ chạy siêu chậm chưa? Không ai có thể tìm ra tại sao nó lại re-render nhiều đến thế, mà có tìm ra thì cũng không ai dám sửa: nó quá rủi ro!
Đã bao giờ bạn thử integrate một thư viện không được viết riêng cho React vào React chưa? Lúc đó bạn có gặp màn hình đơ, CPU tăng vọt vì component bạn viết bị re-render đi re-render quá nhiều lần lại mà không hiểu tại sao không?
Nếu mình nói bạn hãy sử dụng một framework khác để giải quyết những vấn đề trên, có lẽ bạn sẽ lắc đầu ngao ngán: "Lại một framework JS nữa à? Lại phải học một thứ hoàn toàn mới rồi phải re-training lại một team hả?"
Đừng lo, Solid JS sẽ giúp bạn giải quyết những vấn đề đau đầu đó. Solid JS có syntax gần như tương tự với React. Điều này có nghĩa là những developer đã từng code React gần như có thể bắt đầu code trong một dự án Solid JS ngay lập tức.
Component React so sánh với component Solid
Tuyệt vời hơn, vì Solid JS chạy rất nhanh, bạn sẽ không bao giờ phải lo đến vấn đề performance do re-render quá nhiều lần nữa!
Ngoài ra Solid còn cho phép bạn dễ dàng integrate với những thư viện không được viết riêng cho React. Nói một cách khác, Solid cho phép bạn kết hợp cách code quen thuộc của React với những thư viện và cách code "xưa" của Javascript thuần. Và bạn cũng không bao giờ phải lo về useRef
, memo
,... nữa.
Ở trong bài viết này mình sẽ giải thích Solid JS giúp cuộc sống những Frontend developer như mình dễ chịu hơn như thế nào và cùng làm quen với những syntax cơ bản của Solid JS, và làm sao để "tái tạo" Redux và Context trong Solid.
Đầu tiên, hãy cùng tìm hiểu những vấn đề đau đầu mà một lập trình viên React gặp phải mà Solid giải quyết là gì.
Pain points của React
Vấn đề bị re-rendering quá nhiều (ví dụ khi resizing, drag'n'drop, form có quá nhiều field...)
Đã bao giờ bạn thử tự viết một component mà có thể resize ở trong React chưa? Nếu mình bảo bạn viết một component có thể resize được để chứa một component rất nặng khác, bạn sẽ làm thế nào?
Có thể bạn sẽ nghĩ thế này: thay đổi state, và React sẽ thay đổi UI tương ứng cho bạn. Và như thế có lẽ bạn sẽ code thế này.
import React, { useState, useRef } from 'react'; export const ResizableComponent: React.FC = () => { const [width, setWidth] = useState<number>(100); const containerRef = useRef<HTMLDivElement>(null); const onDrag = (e: MouseEvent) => { const delta = e.pageX - containerRef.current!.getBoundingClientRect().right; setWidth((prevWidth) => prevWidth + delta); }; return ( <div ref={containerRef} style={{ width }}> <ExpensiveComponent /> {/* Handle */} <div onMouseDown={() => { document.addEventListener("mousemove", onDrag); document.addEventListener("mouseup", () => { document.removeEventListener("mousemove", onDrag); }); }} /> </div> ); }
Nhưng trời ơi nó lag! Nó lag là vì <ExpensiveComponent/>
bị re-render quá nhiều lần khi bạn drag. Để tránh điều này, bạn cần dùng ref để lưu giá trị của width của component thay vì state.
Bạn nên đặt mọi thứ vào trong state, trừ những thứ thay đổi cả nghìn lần mỗi giây.
Vấn đề re-render quá nhiều lần này cũng xảy ra khi drag and drop, re-render một form có đến hàng vài chục field mỗi lần bạn gõ vào một field,... Bạn cần phải sử dụng ref và đảm bảo làm sao kết hợp state và ref hoạt động một cách hài hòa.
Integrate với những thư viện không được viết riêng cho React
Đã bao giờ bạn integrate những thư viện không được viết riêng cho React vào React chưa? (ví dụ: D3, GSAP, VisJS Timeline,...)
Nếu những thư viện này không có hướng dẫn làm thế nào để integrate vào React, hay không có thư viện chỉ dành riêng cho React, có lẽ bạn sẽ chật vật để integrate nó vào React. Component bạn viết cứ bị re-render đi re-render lại, mà không có useRef
, useEffect
, useMemo
, useCallback
,... nào giúp được cả.
Đôi khi để integrate những thư viện đơn giản bên ngoài vào bạn cần khá hiểu biết về React!
Nếu như bạn ước những vấn đề trên được giải quyết mà không cần phải học một thư viện hoàn toàn mới khác, chào mừng bạn đến với Solid JS.
Tại sao bạn nên thử dùng Solid JS?
Syntax rất rất giống với React
Bạn không có thời gian học một framework mới? Re-traning một team để học một framework mới không khả thi?
Đừng lo! Chỉ cần thay useState
bằng createSignal
, useEffect
bằng createEffect
, bạn có thể bắt đầu code với Solid JS được rồi!
export const App = () => { const [count, setCount] = createSignal<number>(1); return ( <div> <div>{count()}</div> <button onClick={() => { setCount((prevState) => prevState + 1); }} > Increment </button> </div> );
};
Và tiếp theo bạn hãy nhớ rằng đừng destructuring props như trong React thế này là được:
// NOT OK!
const Component = ({ data, onClick }) => {} // OK!
const Component = (props) => { props.data; props.onClick();
}
Trong phần lớn các trường hợp, bạn chỉ cần ghi nhớ 2 điều trên là đủ!
Tất nhiên Solid JS còn khác React ở vài điểm nhỏ nữa ít gặp nữa, nhưng những lúc đó bạn chỉ cần xem documentation là được.
Không cần phải lo lắng đến việc re-render quá nhiều lần
Hãy cùng viết thử component resizable trên nhưng bằng Solid JS:
import { createSignal, type VoidComponent } from "solid-js"; const App: VoidComponent = () => { const [width, setWidth] = createSignal<number>(100); let containerRef!: HTMLDivElement; const onDrag = (e: MouseEvent) => { if (containerRef) { const delta = e.pageX - containerRef.getBoundingClientRect().right; setWidth((prevWidth) => prevWidth + delta); } }; return ( <div ref={containerRef} style={{ width: `${width()}px`, }} > <ExpensiveComponent /> <div onMouseDown={() => { document.addEventListener("mousemove", onDrag); document.addEventListener("mouseup", () => { document.removeEventListener("mousemove", onDrag); }); }} /> </div> );
};
Mặc dù mình viết giống hệt như component React trên: đặt mọi thứ vào trong state (trong Solid sẽ là signal), nhưng nếu bạn thử resize component này, bạn sẽ thấy nó chạy rất mượt mà. Lý do là vì ExpensiveComponent
không bị re-render cả nghìn lần một giây khi bạn resize.
Solid JS không re-render một cách thừa thãi mà chỉ re-render những gì cần phải re-render. Cái này người ta gọi là "fine-grained reactivity" (chắc vậy!).
Với Solid JS, bạn hoàn toàn có thể đặt mọi thứ vào trong state mà không cần phải lo lắng về performance và những thứ như ref
useCallback
, useMemo
nữa.
Dễ dàng integrate với những thư viện viết bằng Vanilla Javascript
Bạn cần integrate thư viện vis-timline vào trong project của mình nhưng thư viện này lại không có hướng dẫn làm sao để integrate vào React và cũng không có thư viện riêng dành cho React?
Với Solid JS, bạn chỉ cần ref đến một div container rồi đặt tất cả những code Javascript thuần vào trong onMount
(giống như useEffect
với dependencies là array rỗng) là xong:
import { onMount, type VoidComponent } from "solid-js";
import { type DataItem, Timeline } from "vis-timeline";
import "vis-timeline/dist/vis-timeline-graph2d.min.css"; const App: VoidComponent = () => { let containerRef!: HTMLDivElement; onMount(() => { // Items const items = [ { id: 1, content: "item 1", start: "2014-04-20" }, { id: 2, content: "item 2", start: "2014-04-14" }, { id: 3, content: "item 3", start: "2014-04-18" }, { id: 4, content: "item 4", start: "2014-04-16", end: "2014-04-19" }, { id: 5, content: "item 5", start: "2014-04-25" }, { id: 6, content: "item 6", start: "2014-04-27", type: "point" }, ]; // Create a Timeline const timeline = new Timeline(containerRef, items, {}); // Do whatever you need with timeline here }); return ( <div ref={containerRef} style={{ width: `600px`, height: `400px`, }} /> );
}
Đây là kết quả của đoạn code trên:
Bạn cần sử dụng D3js?
import { onMount, type VoidComponent } from "solid-js";
import * as d3 from "d3"; const D3Demo: VoidComponent = () => { let container!: HTMLDivElement; onMount(() => { // Declare the chart dimensions and margins. const width = 640; const height = 400; const marginTop = 20; const marginRight = 20; const marginBottom = 30; const marginLeft = 40; // Declare the x (horizontal position) scale. const x = d3 .scaleUtc() .domain([new Date("2023-01-01"), new Date("2024-01-01")]) .range([marginLeft, width - marginRight]); // Declare the y (vertical position) scale. const y = d3 .scaleLinear() .domain([0, 100]) .range([height - marginBottom, marginTop]); // Create the SVG container. const svg = d3.create("svg").attr("width", width).attr("height", height); // Add the x-axis. svg .append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(d3.axisBottom(x)); // Add the y-axis. svg .append("g") .attr("transform", `translate(${marginLeft},0)`) .call(d3.axisLeft(y)); // Append the SVG element. container.append(svg.node()!); }); return <div ref={container} />;
};
Đây là kết quả của đoạn code trên:
Tất cả những gì mình làm ở trên là tạo một div rồi ref đến nó, rồi copy paste code ở trong documentation vào onMount
! Lý do mà mình có thể đơn giản làm như thế này là vì mỗi component trong Solid JS chỉ mount một lần.
Solid JS cho phép bạn kết hợp cả cách viết bằng Javascript thuần thông thường kết hợp với cách viết bằng component như React.
Nhưng còn store (Redux) và context trong Solid JS thì làm thế nào?
Tuyệt vời, mình muốn thử dùng Solid trong project của mình, nhưng còn store và context thì ở trong Solid làm như thế nào?
Bạn có biết rằng trong Solid bạn có thể đặt state ở bên ngoài một component như thế này không?
const [count, setCount] = createSignal<number>(1); export const App = () => { return ( <div> <div>{count()}</div> <button onClick={() => { setCount((prevState) => prevState + 1); }} > Increment </button> </div> );
};
Mình sẽ giải thích điều này giúp giải quyết vấn đề Store management và Context một cách đơn giản hơn React rất nhiều thế nào.
Context
Giả sử bạn có một state mà cả component A và B đều cần sử dụng, bạn chỉ cần đặt nó bên ngoài A và B như sau:
// State (outside of component A and B)
const [count, setCount] = createSignal<number>(1); // Component A
export const ComponentA = () => { return ( <div> <div>{count()}</div> <button onClick={() => { setCount((prevState) => prevState + 1); }} > Increment </button> </div> );
}; // Component B
export const ComponentB = () => { return ( <div> <div>{count()}</div> <button onClick={() => { setCount((prevState) => prevState - 1); }} > Decrement </button> </div> );
};
Bạn không cần phải lo đến props drilling, lift up state, rồi các vấn đề liên quan đến performance khi dùng Context trong React nữa!
Redux
Với Redux, bạn có thể dùng createStore
. createStore
được thiết kế ra cho những data có cấu trúc phức tạp.
import { createSignal, For, onMount, type VoidComponent } from "solid-js";
import { createStore, produce } from "solid-js/store"; // Imagine this is a slice
const [todos, setTodos] = createStore<string[]>([]); // Imagine this is selector
export const selectTodos = () => { return todos;
}; // Imagine this is a dispatcher
export const addTodo = (newTodo: string) => { setTodos(produce((state) => state.push(newTodo)));
}; // Add todo component
const AddTodo: VoidComponent = () => { const [todo, setTodo] = createSignal<string>(""); return ( <> <input onInput={(e) => { setTodo(e.currentTarget.value); }} /> <button onClick={() => { addTodo(todo()); }} > Add todo </button> </> );
}; // List todo component
const TodoList: VoidComponent = () => { const todos = selectTodos(); return <For each={todos}>{(todo) => <div>{todo}</div>}</For>;
};
Và đó là cách bạn có thể quản lý store một cách rất tương tự như bạn dùng Redux trong React.
Nhưng còn điểm dở là gì?
Vì Solid JS không phổ biến như React, ecosystem của nó thiếu nhiều các thư viện tương tự bên React như react-flow, react-three-fiber, react-input-number (thư viện validate và masking cho input),...
Nếu bạn cần SSR, Solid JS có Solid Start, giống như NextJS, nhưng tất nhiên là chưa được battle-tested như NextJS.
Kết luận
Lần đầu tiên thử dùng Solid JS, mình cảm giác không thể tin được có một framework kỳ diệu đến như vậy. Dù mình làm bất kỳ thứ gì, resize, drag and drop, dùng thư viện bên ngoài, viết mọi thứ như là Javascript thuần thông thường, Solid JS đều hoạt động đúng như ý mình muốn, và còn chạy nhanh là đằng khác. Mình có thể code nhanh và chỉ tập trung vào xây dựng ứng dụng mà không phải tính toán đến performance, quy tắc này kia,...
Với việc hầu như không cần phải học thêm gì mới, mọi thứ lúc nào cũng chạy rất nhanh, có thể dễ dàng kết hợp cách code component của React với cách viết bình thường của Javascript thuần, mình cảm giác Solid JS giống như một phiên bản hoàn hảo của React. Mình nghĩ là trong tương lai Solid JS sẽ còn nhanh hơn và có ecosystem rộng hơn nhiều nữa.
Nếu bạn có một project cá nhân nhỏ, hay một dự án MVP cần code nhanh, nhưng lại phải có performance và quen thuộc với nhưng lập trình viên vốn đang sử dụng React, hãy thử dùng Solid JS!
Credits
Nếu bạn thích con cá xinh xắn mà mình dùng trong các hình vẽ, hãy xem cả collection ở đây: https://thenounproject.com/browse/collection-icon/stripe-emotions-106667/.