Mô hình tư duy React làm cho React trở nên đơn giản: nắm những ý chính về component, render, state và effect, rồi áp dụng để xây UI nhanh qua chat.

React có thể gây bực bội lúc đầu vì bạn thấy UI thay đổi nhưng không luôn giải thích được vì sao nó thay đổi. Bạn bấm nút, có thứ gì đó cập nhật, rồi phần khác của trang làm bạn ngạc nhiên. Thường đó không phải là “React kỳ quặc.” Đó là “hình dung của tôi về việc React đang làm mơ hồ.”
Một mô hình tư duy là câu chuyện đơn giản bạn tự kể về cách một thứ hoạt động. Nếu câu chuyện sai, bạn sẽ đưa ra quyết định tự tin mà dẫn đến kết quả khó hiểu. Nghĩ về bộ điều nhiệt: mô hình tệ là “tôi đặt 22°C nên phòng sẽ ngay lập tức là 22°C.” Mô hình tốt hơn là “tôi đặt mục tiêu, và máy sưởi bật/tắt theo thời gian để đạt đến đó.” Với câu chuyện tốt hơn, hành vi không còn cảm thấy ngẫu nhiên.
React cũng vậy. Khi bạn nắm vài ý tưởng rõ ràng, React trở nên có thể dự đoán: bạn có thể nhìn vào dữ liệu hiện tại và đoán chính xác những gì sẽ hiển thị trên màn hình.
Dan Abramov đã giúp phổ biến cách nghĩ “làm cho mọi thứ có thể dự đoán”. Mục tiêu không phải nhớ thuộc lòng hàng loạt luật. Mục tiêu là giữ một vài chân lý nhỏ trong đầu để bạn debug bằng lý luận, không phải bằng thử-sai.
Giữ những ý sau trong đầu:
Nắm được những điều này và React ngừng cảm thấy như phép màu. Nó bắt đầu trông như một hệ thống bạn có thể tin cậy.
React dễ hơn khi bạn ngừng nghĩ theo “màn hình” và bắt đầu nghĩ theo các mảnh nhỏ. Component là một đơn vị UI tái sử dụng. Nó nhận đầu vào và trả về mô tả UI nên trông như thế nào cho những đầu vào đó.
Hữu ích khi coi một component như một mô tả thuần túy: “với dữ liệu này, hiển thị thế này.” Mô tả đó có thể dùng ở nhiều chỗ vì nó không phụ thuộc vào nơi nó nằm.
Props là các đầu vào. Chúng đến từ component cha. Props không bị “sở hữu” bởi component, và component không nên lặng lẽ thay đổi chúng. Nếu một nút nhận label="Save", nhiệm vụ của nút là render nhãn đó, không phải tự quyết định nó nên khác.
State là dữ liệu sở hữu. Nó là thứ component nhớ theo thời gian. State thay đổi khi người dùng tương tác, khi một request hoàn tất, hoặc khi bạn quyết định điều gì đó nên khác. Khác với props, state thuộc về component đó (hoặc component bạn chọn làm chủ).
Phiên bản ngắn gọn của ý chính: UI là một hàm của state. Nếu state nói “loading”, hiển thị spinner. Nếu state nói “error”, hiển thị thông báo. Nếu state nói “items = 3”, render ba hàng. Nhiệm vụ của bạn là giữ UI đọc từ state, không trôi vào các biến ẩn.
Cách tách các khái niệm nhanh:
SearchBox, ProfileCard, CheckoutForm)name, price, disabled)isOpen, query, selectedId)Ví dụ: một modal. Cha có thể truyền title và onClose như props. Modal có thể sở hữu isAnimating như state.
Ngay cả khi bạn đang sinh UI qua chat (ví dụ trên Koder.ai), sự phân tách này vẫn là cách nhanh nhất để giữ đầu óc tỉnh táo: quyết định trước cái gì là props vs state, rồi để UI theo.
Một cách hữu ích để giữ React trong đầu (rất mang tinh thần Dan Abramov) là: render là một phép tính, không phải một thao tác vẽ. React chạy hàm component của bạn để tính xem UI nên trông như thế nào cho props và state hiện tại. Kết quả là một mô tả UI, không phải pixel.
Một re-render chỉ có nghĩa React lặp lại phép tính đó. Nó không đồng nghĩa “toàn bộ trang được vẽ lại.” React so sánh kết quả mới với kết quả trước đó và áp dụng tập thay đổi nhỏ nhất lên DOM thật. Nhiều component có thể re-render trong khi chỉ vài node DOM thực tế cập nhật.
Hầu hết re-render xảy ra vì vài lý do đơn giản: state của component thay đổi, props thay đổi, hoặc cha re-render và React yêu cầu con render lại. Lý do cuối cùng gây ngạc nhiên cho nhiều người, nhưng thường không sao. Nếu bạn coi render là “rẻ và tẻ nhạt”, app của bạn dễ lý giải hơn.
Quy tắc đơn giản giữ mọi thứ rõ ràng: làm cho render thuần túy. Với cùng đầu vào (props + state), component của bạn nên trả về cùng một mô tả UI. Giữ mọi sự bất ngờ ra khỏi render.
Ví dụ cụ thể: nếu bạn sinh ID bằng Math.random() trong render, một re-render sẽ thay đổi ID đó và bỗng nhiên checkbox mất focus hoặc một mục trong danh sách remount. Tạo ID một lần (state, memo, hoặc bên ngoài component) và render sẽ ổn định.
Nếu bạn nhớ một câu: re-render nghĩa là “tính lại UI nên thế nào,” chứ không phải “xây lại mọi thứ.”
Một mô hình hữu ích khác: cập nhật state là các yêu cầu, không phải gán ngay. Khi bạn gọi setter như setCount(count + 1), bạn đang yêu cầu React lên lịch render với giá trị mới. Nếu bạn đọc state ngay sau đó, bạn có thể vẫn thấy giá trị cũ vì React chưa render xong.
Đó là lý do vì sao các cập nhật “nhỏ và có thể dự đoán” quan trọng. Thích mô tả thay đổi hơn là chộp lấy cái bạn nghĩ là giá trị hiện tại. Khi giá trị tiếp theo phụ thuộc vào giá trị trước, dùng dạng updater: setCount(c => c + 1). Nó khớp với cách React hoạt động: nhiều cập nhật có thể được xếp hàng, rồi áp dụng theo thứ tự.
Bất biến (immutability) là nửa còn lại của bức tranh. Đừng thay đổi object hay mảng tại chỗ. Tạo một bản mới với thay đổi. React khi đó có thể nhìn thấy “giá trị này là mới,” và bạn có thể lần theo thứ gì đã thay đổi.
Ví dụ: toggle một todo. Cách an toàn là tạo một mảng mới và một object todo mới cho mục bạn thay đổi. Cách rủi ro là lật todo.done = !todo.done trong mảng hiện có.
Cũng hãy giữ state tối thiểu. Một bẫy phổ biến là lưu những giá trị bạn có thể tính. Nếu bạn đã có items và filter, đừng lưu filteredItems trong state. Tính nó trong render. Ít biến state hơn nghĩa là ít cách để các giá trị trôi lệch.
Kiểm tra đơn giản cho cái gì thuộc về state:
Nếu bạn đang xây UI qua chat (bao gồm trên Koder.ai), hãy yêu cầu các thay đổi dưới dạng bản vá nhỏ: “Thêm một flag boolean” hoặc “Cập nhật danh sách này một cách immutable.” Những thay đổi nhỏ, rõ ràng giữ cho trình sinh và code React của bạn đồng bộ.
Render mô tả UI. Effects đồng bộ với thế giới bên ngoài. “Bên ngoài” nghĩa là những thứ React không kiểm soát: gọi mạng, timers, API trình duyệt, và đôi khi công việc DOM mang tính mệnh lệnh.
Nếu thứ gì đó có thể tính từ props và state, thường nó không nên nằm trong effect. Đặt nó vào effect thêm một bước nữa (render, chạy effect, set state, render lại). Bước bổ sung này là nơi xuất hiện nhấp nháy, vòng lặp, và lỗi “tại sao cái này cũ rồi?”
Sự nhầm lẫn phổ biến: bạn có firstName và lastName, và bạn lưu fullName vào state bằng một effect. Nhưng fullName không phải side effect. Đó là dữ liệu có thể suy ra. Tính nó trong render và nó luôn khớp.
Thói quen: suy ra giá trị UI trong render (hoặc useMemo khi thực sự tốn kém), và dùng effect cho công việc “làm gì đó”, không phải “tìm ra cái gì đó”.
Hãy coi mảng dependency như: “Khi những giá trị này thay đổi, re-sync với thế giới bên ngoài.” Nó không phải mánh hiệu năng và không phải chỗ để dập cảnh báo.
Ví dụ: nếu bạn fetch chi tiết user khi userId thay đổi, userId nên có trong mảng dependency vì nó phải kích hoạt việc sync. Nếu effect dùng token nữa, hãy bao gồm nó, nếu không bạn có thể fetch với token cũ.
Một kiểm tra trực quan tốt: nếu bỏ effect chỉ khiến UI sai đi, có lẽ đó không phải là effect thật sự. Nếu bỏ nó sẽ dừng timer, hủy subscription, hoặc bỏ qua fetch, thì có lẽ đó là effect đúng chỗ.
Một trong những mô hình tư duy hữu ích nhất rất đơn giản: dữ liệu đi xuống cây, và hành động người dùng đi lên.
Cha truyền giá trị xuống con. Con không nên bí mật “sở hữu” cùng một giá trị ở hai nơi. Con yêu cầu thay đổi bằng cách gọi một hàm, và cha quyết định giá trị mới là gì.
Khi hai phần UI phải đồng ý với nhau, chọn một chỗ lưu giá trị, rồi truyền xuống. Đó là “lifting state.” Nó có thể trông như thêm đường ống, nhưng ngăn một vấn đề tồi tệ hơn: hai state trôi lệch và bạn phải thêm mánh để giữ chúng đồng bộ.
Ví dụ: một ô tìm kiếm và danh sách kết quả. Nếu input lưu query của riêng nó và danh sách cũng lưu query riêng, bạn sẽ thấy “input hiển thị X nhưng list dùng Y.” Cách sửa là giữ query ở một parent, truyền xuống cả hai, và truyền onChangeQuery(newValue) về input.
Lifting state không phải lúc nào cũng là câu trả lời. Nếu một giá trị chỉ quan trọng bên trong một component, hãy giữ nó ở đó. Giữ state gần nơi dùng thường làm code dễ đọc hơn.
Ranh giới thực dụng:
Nếu bạn không chắc có nên lift hay không, tìm dấu hiệu: hai component hiển thị cùng một giá trị theo cách khác nhau; một hành động ở chỗ này phải cập nhật thứ xa; bạn sao chép props vào state “phòng khi cần”; hoặc bạn thêm effect chỉ để giữ hai giá trị đồng bộ.
Mô hình này cũng giúp khi xây qua chat tools như Koder.ai: yêu cầu một chủ duy nhất cho mỗi phần shared state, rồi sinh handlers chảy lên trên.
Chọn một tính năng đủ nhỏ để bạn có thể nắm. Một ví dụ tốt là danh sách có tìm kiếm, click một mục để xem chi tiết trong modal.
Bắt đầu bằng vẽ các phần UI và các sự kiện có thể xảy ra. Đừng nghĩ về code. Nghĩ về người dùng có thể làm gì và xem gì: có input tìm kiếm, một danh sách, một dòng được chọn, và một modal. Các sự kiện là gõ tìm kiếm, click một mục, mở modal, đóng modal.
Giờ “vẽ state”. Viết ra vài giá trị phải lưu và quyết định ai sở hữu chúng. Một quy tắc đơn giản hiệu quả: cha chung gần nhất của mọi nơi cần giá trị nên sở hữu nó.
Với tính năng này, state lưu có thể rất ít: query (string), selectedId (id hoặc null), và isModalOpen (boolean). Danh sách đọc query và render các mục. Modal đọc selectedId để hiển thị chi tiết. Nếu cả list và modal cần selectedId, giữ nó ở parent, không ở cả hai.
Tiếp theo, tách dữ liệu suy ra khỏi dữ liệu lưu. Danh sách lọc là dữ liệu suy ra: filteredItems = items.filter(...). Đừng lưu nó trong state vì luôn có thể tính lại từ items và query. Lưu dữ liệu suy ra là cách để các giá trị trôi ra khỏi sync.
Chỉ sau đó hỏi: có cần effect không? Nếu items đã có trong bộ nhớ, không cần. Nếu gõ query phải fetch kết quả, thì có. Nếu đóng modal phải lưu gì đó, thì có. Effect để sync (fetch, save, subscribe), không phải để đấu dây UI cơ bản.
Cuối cùng, test flow với vài cạnh:
selectedId còn hợp lệ không?Nếu bạn trả lời được mấy câu đó trên giấy, code React thường khá thẳng.
Phần lớn nhầm lẫn với React không phải do cú pháp. Nó xảy ra khi code ngừng khớp với câu chuyện đơn giản trong đầu bạn.
Lưu derived state. Bạn lưu fullName trong state dù nó chỉ là firstName + lastName. Nó chạy được cho tới khi một trường thay đổi mà trường kia không, và UI hiện giá trị cũ.
Vòng lặp effect. Một effect fetch dữ liệu, set state, và mảng dependency khiến nó chạy lại. Triệu chứng là request lặp, UI giật, hoặc state không dừng.
Closure cũ (stale closures). Handler click đọc giá trị cũ (như counter hoặc filter đã lỗi thời). Triệu chứng: “Tôi click nhưng nó dùng giá trị của hôm qua.”
Global state khắp nơi. Đặt mọi chi tiết UI vào store toàn cục khiến khó biết cái gì sở hữu gì. Triệu chứng: bạn thay một thứ và ba màn hình khác phản ứng bất ngờ.
Mutating object lồng nhau. Bạn cập nhật object hoặc mảng tại chỗ và tự hỏi tại sao UI không cập nhật. Triệu chứng: “dữ liệu đã thay đổi nhưng không re-render.”
Ví dụ cụ thể: panel “tìm kiếm và sắp xếp” cho một danh sách. Nếu bạn lưu filteredItems trong state, nó có thể trôi so với items khi dữ liệu mới đến. Thay vào đó, lưu các inputs (text tìm kiếm, lựa chọn sắp xếp) và tính danh sách lọc trong render.
Với effects, giữ chúng để sync với thế giới ngoài (fetching, subscriptions, timers). Nếu một effect làm công việc UI cơ bản, nó thường thuộc về render hoặc handler sự kiện.
Khi sinh hoặc sửa code qua chat, những lỗi này hiện lên nhanh hơn vì thay đổi có thể đến thành khối lớn. Thói quen tốt là diễn tả yêu cầu theo chủ sở hữu: “Nguồn sự thật cho giá trị này là gì?” và “Chúng ta có thể tính ra thay vì lưu không?”
Khi UI bắt đầu cảm thấy không dự đoán được, hiếm khi là “React quá nhiều.” Thường là quá nhiều state, ở chỗ sai, làm việc nó không nên làm.
Trước khi thêm useState nữa, dừng lại và hỏi:
Ví dụ nhỏ: ô tìm kiếm, dropdown filter, danh sách. Nếu bạn lưu cả query và filteredItems trong state, bạn có hai nguồn sự thật. Thay vào đó, giữ query và filter làm state, rồi suy ra filteredItems trong render từ danh sách đầy đủ.
Điều này quan trọng khi bạn build nhanh qua chat tools nữa. Tốc độ rất tốt, nhưng cứ hỏi: “Chúng ta có thêm state, hay vô tình thêm một giá trị suy ra?” Nếu là suy ra, xóa state đó và tính nó.
Một nhóm nhỏ đang xây UI admin: bảng orders, vài filter, và dialog chỉnh sửa order. Yêu cầu ban đầu mơ hồ: “Thêm filters và popup edit.” Nghe có vẻ đơn giản, nhưng thường biến thành state rải rác khắp nơi.
Làm cho nó cụ thể bằng cách chuyển yêu cầu thành state và events. Thay vì “filters”, đặt tên state: query, status, dateRange. Thay vì “edit popup”, đặt tên event: “user clicks Edit on a row.” Rồi quyết định ai sở hữu mỗi phần state (page, table, hoặc dialog) và cái gì có thể suy ra (như filtered list).
Ví dụ prompt giữ mô hình nguyên vẹn (cũng hữu ích cho trình tạo chat như Koder.ai):
OrdersPage sở hữu filters và selectedOrderId. OrdersTable được điều khiển bởi filters và gọi onEdit(orderId).”visibleOrders từ orders và filters. Đừng lưu visibleOrders trong state.”EditOrderDialog nhận order và open. Khi lưu, gọi onSave(updatedOrder) và đóng.”filters vào URL, không để tính hàng đã lọc.”Sau khi UI được sinh hoặc cập nhật, rà soát nhanh: mỗi giá trị state có một chủ, giá trị suy ra không bị lưu, effect chỉ để sync với bên ngoài (URL, network, storage), và events chảy xuống dưới dạng props và lên trên như callbacks.
Khi state dự đoán được, việc lặp trở nên an toàn. Bạn có thể thay layout bảng, thêm filter mới, hoặc chỉnh trường dialog mà không đoán xem state ẩn nào sẽ vỡ tiếp theo.
Tốc độ chỉ hữu ích nếu app vẫn dễ lý giải. Bảo vệ đơn giản nhất là xem những mô hình tư duy này như một checklist áp dụng trước khi bạn viết (hoặc sinh) UI.
Bắt đầu mỗi tính năng cùng cách: viết ra state cần, các event thay đổi nó, và ai sở hữu nó. Nếu bạn không thể nói “Component này sở hữu state này, và các event này cập nhật nó”, bạn có khả năng kết thúc với state rải rác và re-render bất ngờ.
Nếu bạn xây qua chat, bắt đầu bằng chế độ lập kế hoạch. Mô tả components, hình dạng state, và các chuyển đổi bằng ngôn ngữ đơn giản trước khi yêu cầu code. Ví dụ: “Panel lọc cập nhật query state; danh sách kết quả suy ra từ query; chọn một mục đặt selectedId; đóng sẽ xóa nó.” Khi đọc thấy rõ, sinh UI trở thành bước cơ học.
Nếu bạn dùng Koder.ai (koder.ai) để sinh code React, đáng để thực hiện rà soát nhanh: một chủ rõ ràng cho mỗi giá trị state, UI suy ra từ state, effect chỉ để sync, và không có nguồn sự thật trùng lặp.
Rồi lặp theo từng bước nhỏ. Nếu bạn muốn thay cấu trúc state (ví dụ từ vài boolean sang một trường status duy nhất), chụp snapshot trước, thử nghiệm, và quay lại nếu mô hình tư duy tệ hơn. Khi cần rà soát sâu hơn hoặc bàn giao, xuất source code giúp dễ trả lời câu hỏi thực sự: cấu trúc state còn kể đúng câu chuyện UI không?
Một mô hình khởi đầu tốt là: UI = f(state, props). Các component của bạn không “điều khiển DOM”; chúng mô tả những gì nên hiển thị trên màn hình cho dữ liệu hiện tại. Nếu màn hình sai, hãy kiểm tra state/props đã tạo ra nó, chứ không phải DOM.
Props là đầu vào từ component cha; component của bạn nên coi chúng là chỉ đọc. State là bộ nhớ do một component (hoặc component bạn chọn làm chủ) sở hữu. Nếu một giá trị cần được chia sẻ, nâng nó lên (lift) và truyền xuống dưới dạng props.
Một re-render nghĩa là React chạy lại hàm component để tính mô tả UI tiếp theo. Nó không tự động đồng nghĩa với việc toàn bộ trang được vẽ lại. React sau đó cập nhật DOM thật với tập thay đổi nhỏ nhất cần thiết.
Bởi vì cập nhật state là được lập lịch, không phải gán ngay lập tức. Nếu giá trị tiếp theo phụ thuộc vào giá trị trước, hãy dùng dạng updater để không phụ thuộc vào giá trị có thể đã cũ:
setCount(c => c + 1)Cách này vẫn đúng ngay cả khi nhiều cập nhật được xếp hàng.
Tránh lưu bất cứ thứ gì bạn có thể tính từ các đầu vào hiện có. Lưu các đầu vào, suy ra phần còn lại trong khi render.
Ví dụ:
items, filtervisibleItems = items.filter(...)Cách này ngăn các giá trị trôi lệch khỏi nhau.
Dùng effect để đồng bộ với những thứ React không kiểm soát: fetch, subscribe, timers, API của trình duyệt, hoặc công việc DOM mang tính mệnh lệnh.
Đừng dùng effect chỉ để tính giá trị UI từ state—tính chúng trong render (hoặc useMemo nếu quá tốn kém).
Hãy xem mảng dependency như một danh sách kích hoạt: “khi những giá trị này thay đổi, hãy re-sync”. Bao gồm mọi giá trị phản ứng mà effect đọc. Nếu bỏ sót, bạn có thể dùng dữ liệu cũ (như userId hay token cũ). Nếu thêm sai, bạn có thể tạo vòng lặp—thường là dấu hiệu effect đang làm công việc thuộc về event hoặc render.
Nếu hai phần UI phải luôn đồng bộ, đặt state vào cha chung gần nhất của chúng, truyền giá trị xuống và truyền callback lên. Bài test nhanh: nếu bạn nhân đôi cùng một giá trị trong hai component và viết effect để “giữ chúng đồng bộ”, thì giá trị đó có lẽ cần một chủ duy nhất.
Thường xảy ra khi handler “bắt” một giá trị cũ từ một render trước đó. Sửa phổ biến:
setX(prev => ...)Nếu một click dùng “giá trị hôm qua”, hãy nghi ngờ closure cũ.
Bắt đầu với một kế hoạch nhỏ: component, chủ sở hữu state, và các event. Sinh code dưới dạng các bản vá nhỏ (thêm một trường state, thêm một handler, suy ra một giá trị) thay vì sửa một đống. Nếu dùng trình tạo chat như Koder.ai, yêu cầu:
Cách này giữ cho code sinh ra phù hợp với mô hình tư duy của React.