React.createElement()useTransition, useDeferredValue, SuspenseReact.memo đúng cách để tối ưu renderingVào react.dev/learn và đọc toàn bộ phần "Learn React" từ đầu đến hết. Đây là tài liệu chuẩn nhất, được viết lại hoàn toàn cho React 18+. Đọc kỹ từng mục, đặc biệt phần "Thinking in React", "Describing the UI", "Adding Interactivity", và "Managing State". Thực hành mọi interactive example trên trang.
Bài viết kinh điển tại overreacted.io/a-complete-guide-to-useeffect. Bài này thay đổi cách bạn hiểu về useEffect — không phải lifecycle method, mà là synchronization mechanism. Đọc chậm, đọc lại nhiều lần, vì đây là nền tảng tư duy quan trọng nhất khi làm việc với React hooks.
Đọc repo github.com/acdlite/react-fiber-architecture để hiểu tại sao React team phải viết lại hoàn toàn reconciler. Tập trung vào khái niệm "unit of work", incremental rendering, và cách Fiber cho phép React pause/resume rendering.
Tạo một ứng dụng gồm Counter component và Todo List component. Yêu cầu: sử dụng functional components, quản lý state bằng useState, truyền dữ liệu qua props, xử lý events, áp dụng conditional rendering và list rendering với key prop đúng cách. Không dùng bất kỳ thư viện state management nào.
React component là đơn vị cơ bản nhất — một hàm (hoặc class) nhận props và trả về React elements mô tả UI.
Cách viết truyền thống, kế thừa từ React.Component. Có lifecycle methods như componentDidMount, componentDidUpdate, componentWillUnmount.
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
document.title = `Count: ${this.state.count}`;
}
componentDidUpdate() {
document.title = `Count: ${this.state.count}`;
}
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Increment
</button>
</div>
);
}
}
Cùng logic, viết bằng function + hooks. Ngắn gọn hơn, dễ đọc hơn, dễ compose hơn.
function Welcome() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Tại sao Functional Components thắng?
this, không cần bind methods, không cần constructorMột component "pure" là component luôn trả về cùng output cho cùng input (props + state). React yêu cầu render function phải pure — không side effects trong render phase. Side effects thuộc về useEffect, event handlers, hoặc external systems.
React khuyến khích composition thay vì inheritance. Thay vì tạo SpecialButton extends Button, hãy tạo SpecialButton render Button bên trong và truyền props.
function Dialog({ title, children }) {
return (
<div className="dialog">
<h2>{title}</h2>
<div className="dialog-body">{children}</div>
</div>
);
}
function WelcomeDialog() {
return (
<Dialog title="Chào mừng">
<p>Cảm ơn bạn đã ghé thăm!</p>
</Dialog>
);
}
JSX không phải HTML. JSX là syntax extension cho JavaScript, được Babel/SWC biên dịch thành lời gọi hàm React.createElement().
Khi bạn viết JSX như thế này:
<div className="container">
<h1>Hello</h1>
<p>World</p>
</div>
Babel sẽ biên dịch thành:
React.createElement(
"div",
{ className: "container" },
React.createElement("h1", null, "Hello"),
React.createElement("p", null, "World")
);
Kết quả trả về là một plain JavaScript object (React Element):
{
type: "div",
props: {
className: "container",
children: [
{ type: "h1", props: { children: "Hello" } },
{ type: "p", props: { children: "World" } }
]
}
}
Khi bạn hiểu JSX chỉ là object, bạn sẽ hiểu:
props.children{}) chấp nhận bất kỳ JavaScript expression nào vì nó chỉ là argument cho function callTừ React 17+, JSX transform mới không cần import React nữa. Babel tự thêm import jsx runtime function.
Props là cách component cha truyền dữ liệu xuống component con. Props là immutable — component con không bao giờ được modify props nhận được.
Mọi thứ nằm giữa opening và closing tag trở thành props.children.
function Card({ children, title }) {
return (
<div className="card">
<h3>{title}</h3>
{children}
</div>
);
}
function App() {
return (
<Card title="Profile">
<img src="avatar.png" />
<p>User info here</p>
</Card>
);
}
Truyền function làm prop để component con gọi lại và nhận data.
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
const handleMove = (e) => {
setPos({ x: e.clientX, y: e.clientY });
};
return <div onMouseMove={handleMove}>{render(pos)}</div>;
}
function App() {
return (
<MouseTracker
render={({ x, y }) => <p>Mouse: {x}, {y}</p>}
/>
);
}
Khi dữ liệu phải "xuyên qua" nhiều tầng component trung gian không cần dùng nó, đó là prop drilling. Giải pháp: Context API, state management libraries, hoặc component composition (đưa component cần data lên gần data source hơn).
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserProfile user={user} />;
}
Ở ví dụ trên, Layout và Sidebar không cần user nhưng buộc phải nhận và truyền tiếp. Đây là dấu hiệu cần Context hoặc composition.
Trước React 18, batching chỉ xảy ra trong React event handlers. Từ React 18, mọi state updates đều được batch — kể cả trong setTimeout, Promises, native event handlers.
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
setName("React");
}
Cả 3 setState chỉ trigger 1 lần re-render duy nhất. React gom tất cả updates lại, tính toán state mới, rồi render một lần.
Nếu bạn cần force flush (hiếm khi cần):
import { flushSync } from "react-dom";
function handleClick() {
flushSync(() => {
setCount(c => c + 1);
});
flushSync(() => {
setFlag(f => !f);
});
}
Khi state mới phụ thuộc vào state cũ, luôn dùng functional form để tránh stale closure.
setCount(prevCount => prevCount + 1);
setItems(prevItems => [...prevItems, newItem]);
setUser(prevUser => ({ ...prevUser, name: "New Name" }));
Khi initial state tốn tính toán (đọc localStorage, parse JSON...), truyền function thay vì giá trị để chỉ chạy một lần khi mount.
const [data, setData] = useState(() => {
const saved = localStorage.getItem("data");
return saved ? JSON.parse(saved) : defaultValue;
});
Nếu viết useState(expensiveComputation()), hàm sẽ chạy mỗi lần render dù giá trị chỉ dùng lần đầu.
Virtual DOM là một lightweight copy của Real DOM dưới dạng JavaScript objects (React Elements tree). Mỗi khi state/props thay đổi, React tạo Virtual DOM mới, so sánh (diff) với cây cũ, rồi chỉ cập nhật những phần thay đổi lên Real DOM.
So sánh 2 cây tùy ý có complexity O(n³). React giảm xuống O(n) bằng 2 giả định:
Quy trình cụ thể:
<div> → <span>), destroy toàn bộ subtree cũKhi render list, React cần biết element nào là "cùng một thứ" giữa 2 lần render.
const items = ["A", "B", "C"];
<ul>
{items.map(item => (
<li key={item}>{item}</li>
))}
</ul>
Giả sử danh sách [A, B, C] thêm item đầu thành [X, A, B, C]:
Index chỉ an toàn khi: list tĩnh, không reorder, không thêm/xóa đầu/giữa.
Stack Reconciler cũ hoạt động đồng bộ, đệ quy. Khi cây component lớn, quá trình reconciliation block main thread → UI đơ, animation giật, input không phản hồi. Không có cách nào để pause, abort, hoặc ưu tiên công việc.
Fiber là kiến trúc mới của React (từ v16), trong đó mỗi component/element được biểu diễn bằng một Fiber node — một JavaScript object chứa thông tin về component, state, props, và các pointer đến parent/child/sibling.
{
type: Function | String | Symbol,
key: null | String,
stateNode: DOM node | Component instance,
child: Fiber | null,
sibling: Fiber | null,
return: Fiber | null,
pendingProps: Object,
memoizedState: Object,
effectTag: Number,
lanes: Number
}
Thay vì xử lý toàn bộ cây một lượt (stack), Fiber chia công việc thành units of work. React xử lý từng fiber node một, sau mỗi unit có thể:
| Phase | Render (Reconciliation) | Commit |
|---|---|---|
| Có thể bị interrupt? | Có | Không |
| Làm gì? | Tính toán changes (diff) | Apply changes vào DOM |
| Side effects? | Không | Có (DOM mutations, lifecycle) |
React 18 dùng hệ thống "Lanes" để phân loại priority của updates:
useTransition)Lanes cho phép React xử lý high-priority updates trước mà không bị block bởi low-priority work đang chạy.
Đánh dấu state update là "non-urgent" — React sẽ giữ UI cũ hiển thị trong khi tính toán UI mới ở background. Nếu có user input mới, React abort transition đang chạy và xử lý input trước.
function SearchResults() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setQuery(e.target.value);
startTransition(() => {
setResults(filterHugeList(e.target.value));
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <span>Đang tải...</span>}
<ResultList data={results} />
</div>
);
}
isPending là boolean cho biết transition đang chạy, dùng để hiện loading indicator.
Tương tự useTransition nhưng dùng cho giá trị thay vì hành động. React sẽ trì hoãn việc re-render với giá trị mới và hiển thị giá trị cũ trước.
function SearchPage({ query }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<HeavyResultList query={deferredQuery} />
</div>
);
}
| Tiêu chí | useTransition | useDeferredValue |
|---|---|---|
| Control | Wrap setState call | Wrap giá trị nhận được |
| Use case chính | Bạn sở hữu state update | Bạn nhận value từ props/parent |
| Loading state | Có isPending | So sánh value vs deferredValue |
| Ví dụ | Tab switching, filter | Search results từ parent query |
Suspense cho phép component "chờ" một thứ gì đó (data fetching, lazy loading) và hiển thị fallback trong lúc chờ.
const LazyComponent = React.lazy(() => import("./HeavyComponent"));
function App() {
return (
<Suspense fallback={<div>Đang tải...</div>}>
<LazyComponent />
</Suspense>
);
}
Error Boundaries là React components bắt JavaScript errors trong child component tree, log lỗi, và hiển thị fallback UI thay vì crash toàn bộ app.
Chỉ viết được bằng Class Component — React chưa cung cấp hook equivalent.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
logErrorToService(error, errorInfo.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <h2>Đã xảy ra lỗi.</h2>;
}
return this.props.children;
}
}
Sử dụng:
<ErrorBoundary fallback={<p>Lỗi rồi!</p>}>
<UserProfile />
</ErrorBoundary>
Function nhận component, trả về component mới với logic bổ sung. Pattern phổ biến thời class components.
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const isLoggedIn = useAuth();
if (!isLoggedIn) return <LoginScreen />;
return <WrappedComponent {...props} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);
Vấn đề: Wrapper hell (nhiều HOC lồng nhau), khó trace props origin, naming collisions, static methods bị mất.
Truyền function làm prop (hoặc children) để share logic. Giải quyết vấn đề transparency của HOC.
function AuthGuard({ children }) {
const isLoggedIn = useAuth();
if (!isLoggedIn) return <LoginScreen />;
return children;
}
Vấn đề: Vẫn tạo nesting sâu (callback hell trong JSX), khó đọc với nhiều render props.
Extract logic stateful thành function bắt đầu bằng use. Không thay đổi component tree, không wrapper, hoàn toàn composable.
function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAuthStatus().then(u => {
setUser(u);
setLoading(false);
});
}, []);
return { user, loading, isLoggedIn: !!user };
}
function Dashboard() {
const { user, loading, isLoggedIn } = useAuth();
if (loading) return <Spinner />;
if (!isLoggedIn) return <LoginScreen />;
return <DashboardContent user={user} />;
}
Tại sao hooks thắng: Flat code (không nesting), explicit data flow, composable (gọi nhiều hooks tuần tự), dễ test từng hook riêng biệt.
React.memo là Higher-Order Component wrap functional component. Nó thực hiện shallow comparison trên props — nếu props không đổi, skip re-render.
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
Tham số thứ 2 là function so sánh tùy chỉnh. Return true = props giống nhau (skip render), false = props khác (re-render).
const MemoizedItem = React.memo(
function Item({ data, config }) {
return <div>{data.name} - {config.theme}</div>;
},
function arePropsEqual(prevProps, nextProps) {
return (
prevProps.data.id === nextProps.data.id &&
prevProps.config.theme === nextProps.config.theme
);
}
);
| API | Memoize cái gì | Dùng khi nào |
|---|---|---|
React.memo | Component (skip re-render) | Component nhận cùng props thường xuyên |
useMemo | Giá trị tính toán | Expensive computation trong render |
useCallback | Function reference | Truyền callback cho memo-ized child |
Quan trọng: React.memo chỉ hiệu quả khi kết hợp với useCallback/useMemo ở parent. Nếu parent tạo object/function mới mỗi render, shallow comparison luôn thấy "khác" → memo vô dụng.
Virtual DOM là một lightweight JavaScript object representation của Real DOM tree. Mỗi khi state thay đổi, React tạo Virtual DOM mới, diff với cây cũ (thuật toán O(n) với 2 heuristics: khác type = rebuild, key hint cho list), rồi batch apply chỉ những thay đổi cần thiết lên Real DOM.
Tại sao nhanh hơn: Thao tác trên JS objects nhanh hơn nhiều so với DOM APIs. Real DOM manipulation trigger layout/paint/composite — rất expensive. Virtual DOM cho phép React gom nhiều changes lại thành một batch, minimize actual DOM operations. Tuy nhiên, nói "Virtual DOM nhanh hơn" là không chính xác — nó nhanh hơn naive approach (update DOM trực tiếp mỗi khi có thay đổi), nhưng không nhanh hơn hand-optimized DOM manipulation. Giá trị thực sự là declarative API với hiệu năng đủ tốt.
Stack Reconciler cũ xử lý đồng bộ — khi cây component lớn, reconciliation block main thread hàng trăm ms, UI freeze, animation giật, input lag. Không thể pause hay prioritize.
Fiber giải quyết bằng cách biến reconciliation thành incremental, chia thành các unit of work nhỏ. React có thể pause giữa chừng, nhường main thread cho browser (16ms frame budget), rồi resume. Fiber cũng mang đến priority system (Lanes) — user interactions được ưu tiên hơn background data fetching. Đây là nền tảng cho tất cả Concurrent Features trong React 18.
Controlled: React state là "single source of truth". Giá trị input luôn phản ánh state, mỗi thay đổi đi qua event handler → setState → re-render.
function ControlledInput() {
const [value, setValue] = useState("");
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
Uncontrolled: DOM tự quản lý state. Dùng ref để đọc giá trị khi cần (submit form chẳng hạn).
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = () => console.log(inputRef.current.value);
return <input ref={inputRef} defaultValue="" />;
}
Khi nào dùng Uncontrolled: Form đơn giản, file input, tích hợp thư viện non-React. Còn lại, luôn ưu tiên Controlled vì dễ validate, transform, và đồng bộ dữ liệu.
key giúp React identify element nào giữa các lần render. Không có key, React diff children theo thứ tự → thêm/xóa đầu list sẽ re-render tất cả.
Tại sao không dùng index: Khi list thay đổi (thêm/xóa/reorder), index không ổn định — item tại index 0 có thể là item khác hoàn toàn so với render trước. React sẽ map sai state/DOM vào sai item → bug về state, animation sai, hiệu năng kém. Dùng unique, stable identifier (database id, UUID).
React.memo(Component): HOC wrap component, shallow compare props, skip re-render nếu props không đổi. Tác dụng ở component level.
useMemo(() => value, deps): Cache kết quả tính toán expensive bên trong component. Re-compute chỉ khi deps thay đổi.
useCallback(fn, deps): Cache function reference. Tương đương useMemo(() => fn, deps). Dùng khi truyền callback cho child component đã memo.
Mối liên hệ: React.memo chỉ hiệu quả khi parent stabilize props bằng useMemo (cho objects) và useCallback (cho functions). Nếu không, mỗi render tạo reference mới → shallow comparison luôn fail → memo vô dụng.
Trước React 18: Chỉ batch trong React synthetic event handlers. Trong setTimeout, Promises, native event handlers → mỗi setState trigger 1 re-render riêng.
React 18 (Automatic Batching): Batch trong MỌI context — setTimeout, Promises, native events, thậm chí trong cả event listeners bên ngoài React. Giảm số lần re-render đáng kể.
Nếu cần opt-out: dùng flushSync() từ react-dom để force immediate render sau mỗi setState.
Catch được: Errors trong rendering, lifecycle methods, và constructors của cả component tree bên dưới.
KHÔNG catch được:
Lý do: Error Boundary dựa trên React rendering lifecycle. Event handlers và async code chạy ngoài render flow nên React không thể catch.
forwardRef cho phép component truyền ref nhận được xuống cho child DOM element hoặc child component bên trong. Mặc định, ref không phải prop thông thường — React xử lý đặc biệt và không truyền qua props.
Use cases:
const FancyInput = React.forwardRef(function FancyInput(props, ref) {
return <input ref={ref} className="fancy" {...props} />;
});
function Parent() {
const inputRef = useRef(null);
const handleClick = () => inputRef.current.focus();
return <FancyInput ref={inputRef} />;
}
Lưu ý React 19+: ref sẽ trở thành prop thông thường, không cần forwardRef nữa.
React team khuyến cáo luôn dùng composition thay vì inheritance. Tại Facebook (hàng nghìn components), họ chưa tìm thấy use case nào mà inheritance tốt hơn.
Composition đạt được mọi thứ inheritance làm:
<Dialog> → <WelcomeDialog> bằng cách render Dialog với props cụ thểchildren prop cho phép component chứa nội dung bất kỳVấn đề của Inheritance: Tight coupling, brittle hierarchy (thay đổi base class ảnh hưởng tất cả), diamond problem, khó refactor.
useTransition: Wrap một state update để đánh dấu non-urgent. Bạn kiểm soát action (setState). Trả về [isPending, startTransition].
useDeferredValue: Nhận một value và trả về deferred version. Bạn KHÔNG kiểm soát state update — chỉ trì hoãn value mà bạn nhận được (thường từ props).
Quy tắc:
useTransitionuseDeferredValueCả hai đều cho phép React ưu tiên urgent updates (input typing) và trì hoãn expensive re-renders (filtering large list).
Xây dựng Counter component với các yêu cầu:
Acceptance criteria: Counter hoạt động đúng, giá trị persist qua page reload, undo hoạt động chính xác.
Xây dựng Todo application:
crypto.randomUUID())Acceptance criteria: Tất cả CRUD hoạt động, filter hoạt động đúng, không lỗi key warning trong console.
Tạo một reusable ErrorBoundary component:
fallback prop (ReactNode hoặc render function nhận error)onError callback propresetErrorBoundary()BuggyComponent cố tình throw error để testAcceptance criteria: Error được catch, fallback hiển thị, retry hoạt động, onError callback được gọi.
Tạo app có input search filter 10,000 items:
Acceptance criteria: Version không có useTransition bị lag input rõ rệt, version có useTransition input mượt.
Build list 100 product cards. Tách ProductCard, dùng key ổn định, đo re-render khi search text thay đổi. Chỉ dùng React.memo sau khi chứng minh có render thừa.
Build form profile có 3 field. Parent giữ state, child field nhận value/onChange. Refactor để tránh prop drilling quá sâu bằng composition hoặc custom hook.
Bọc từng vùng màn hình bằng ErrorBoundary. Khi một card render lỗi, fallback chỉ thay card đó, không làm crash toàn bộ screen.
React.memo nhưng vẫn truyền object/function inline mới mỗi render.Case: Danh sách address cho checkout dùng index làm key. User xóa address đầu tiên, input đang sửa ở address thứ hai nhảy sang address khác.
Cách xử lý: Dùng id ổn định từ backend/local uuid làm key. Nếu item chưa có id server, tạo clientId ngay khi add và giữ nguyên đến khi sync xong.
React core dùng được trong RN khi bạn kiểm soát identity của component, luồng data, và render cost. Đừng tối ưu trước khi đo, nhưng đừng bỏ qua key và functional update.
React.memo?