Học cách refactor component React với Claude Code bằng tests characterization, các bước nhỏ an toàn và tách rối state để cải thiện cấu trúc mà không thay đổi hành vi.

Refactor React cảm thấy rủi ro vì hầu hết component không phải là các khối nhỏ, sạch sẽ. Chúng là đống sống gồm UI, state, effect, và những sửa chữa “chỉ thêm một prop nữa”. Khi bạn thay đổi cấu trúc, thường bạn vô tình thay đổi thời điểm chạy, identity, hoặc luồng dữ liệu.
Một refactor thường thay đổi hành vi khi nó vô tình:
Refactor cũng biến thành rewrite khi “dọn dẹp” bị trộn lẫn với “cải tiến.” Bạn bắt đầu bằng việc trích một component, rồi đổi tên một đống thứ, rồi “chỉnh” shape của state, rồi thay hook. Sớm thôi bạn thay đổi logic trong khi cũng thay layout. Không có hàng rào bảo vệ, rất khó biết thay đổi nào gây ra bug.
Một refactor an toàn có một lời hứa đơn giản: người dùng nhận cùng hành vi, và bạn kết thúc với code rõ ràng hơn. Props, events, trạng thái loading, trạng thái lỗi và các trường hợp cạnh biên nên hoạt động như trước. Nếu hành vi thay đổi, nó phải có chủ đích, nhỏ, và được ghi rõ.
Nếu bạn refactor component React với Claude Code (hoặc bất kỳ trợ lý code nào), hãy coi nó như đồng lập trình nhanh, chứ không phải chế độ lái tự động. Yêu cầu nó mô tả các rủi ro trước khi sửa, đề xuất kế hoạch với các bước nhỏ an toàn, và giải thích cách nó kiểm tra rằng hành vi vẫn giữ nguyên. Rồi xác thực bằng chính bạn: chạy app, click những đường đi lạ, và dựa vào tests ghi lại những gì component đang làm hôm nay, không phải mong ước của bạn về nó.
Chọn một component mà thực sự đang tốn thời gian của bạn. Không phải toàn bộ trang, không phải “lớp UI,” và không phải một “dọn dẹp mơ hồ.” Chọn một component đơn lẻ khó đọc, khó sửa, hoặc đầy state và side effects mong manh. Mục tiêu hẹp cũng khiến đề xuất của trợ lý dễ xác minh hơn.
Viết một mục tiêu bạn có thể kiểm tra trong năm phút. Mục tiêu tốt nói về cấu trúc, không phải kết quả: “tách thành các component nhỏ hơn,” “làm cho state dễ theo dõi hơn,” hoặc “làm cho testable mà không phải mock nửa app.” Tránh mục tiêu như “làm tốt hơn” hoặc “tăng hiệu năng” trừ khi bạn có metric và nút thắt rõ ràng.
Đặt ranh giới trước khi mở editor. Những refactor an toàn thường nhàm chán:
Rồi liệt kê các phụ thuộc có thể âm thầm phá vỡ hành vi khi bạn di chuyển code: API calls, context providers, routing params, feature flags, analytics events, và trạng thái global chia sẻ.
Ví dụ cụ thể: bạn có OrdersTable 600 dòng fetch data, filter, quản lý selection, và show drawer với chi tiết. Mục tiêu rõ ràng có thể là: “tách việc render row và drawer UI thành component, và đưa selection state vào một reducer, không thay đổi UI.” Mục tiêu đó cho bạn biết “xong” là gì và thứ gì nằm ngoài phạm vi.
Trước khi refactor, hãy coi component như một hộp đen. Nhiệm vụ của bạn là ghi lại nó đang làm gì hôm nay, không phải bạn muốn nó làm gì. Điều này giữ cho refactor không biến thành redesign.
Bắt đầu bằng cách viết hành vi hiện tại bằng ngôn ngữ đơn giản: với các input này, UI hiển thị output kia. Bao gồm props, URL params, feature flags, và bất kỳ dữ liệu nào lấy từ context hoặc store. Nếu dùng Claude Code, dán một đoạn nhỏ, tập trung và yêu cầu nó diễn lại hành vi thành các câu chính xác bạn có thể kiểm tra sau.
Bao phủ các trạng thái UI mà người dùng thực sự thấy. Một component có thể trông ổn trên happy path trong khi phá khi vào loading, empty, hoặc error.
Cũng ghi lại các quy tắc ngầm mà dễ bỏ sót và thường khiến refactor phá hành vi:
Ví dụ: bạn có bảng user load results, hỗ trợ tìm kiếm, và sort theo “Last active.” Viết lại điều gì xảy ra khi search rỗng, khi API trả về danh sách rỗng, khi API lỗi, và khi hai user có cùng giá trị “Last active”. Ghi chi tiết nhỏ như sort có phân biệt chữ hoa/thường không, và bảng có giữ trang hiện tại khi filter thay đổi không.
Khi ghi chú của bạn trở nên nhàm chán và cụ thể, bạn đã sẵn sàng.
Characterization tests là các test “đây là những gì nó làm hôm nay”. Chúng mô tả hành vi hiện tại, ngay cả khi nó kỳ quặc, không nhất quán, hoặc rõ ràng không phải điều bạn muốn về lâu dài. Nghe có vẻ ngược, nhưng điều này ngăn refactor lặng lẽ biến thành rewrite.
Khi refactor component React với Claude Code, những test này là lan can an toàn. Công cụ có thể giúp thay đổi cấu trúc, nhưng bạn quyết định những gì không được thay đổi.
Tập trung vào những gì người dùng (và code khác) phụ thuộc vào:
Để giữ test ổn định, assert kết quả, không phải implementation. Ưu tiên “nút Save bị disabled và xuất hiện thông báo” hơn “setState đã được gọi” hay “hook này chạy”. Nếu test vỡ vì bạn đổi tên component hoặc sắp xếp hooks, tức là nó không bảo vệ hành vi.
Hành vi bất đồng bộ là nơi refactor thường thay đổi timing. Xử lý rõ ràng: chờ UI ổn định rồi assert. Nếu có timers (debounced search, toast delayed), dùng fake timers và tiến thời gian. Nếu có network calls, mock fetch và assert những gì người dùng thấy sau success và failure. Với các luồng giống Suspense, test cả fallback lẫn view sau khi resolve.
Ví dụ: bảng “Users” chỉ hiện “No results” sau khi search hoàn thành. Một characterization test nên khoá chuỗi đó: loading indicator trước, rồi rows hoặc empty, bất kể bạn tách component thế nào sau này.
Thắng lợi không phải là “thay đổi lớn nhanh hơn.” Thắng lợi là có bức tranh rõ ràng component đang làm gì, rồi thay đổi một việc nhỏ tại một thời điểm trong khi giữ hành vi ổn định.
Bắt đầu bằng việc dán component và yêu cầu tóm tắt trách nhiệm bằng tiếng thường. Khuyến khích cụ thể: nó hiển thị dữ liệu gì, xử lý hành động người dùng nào, và kích hoạt side effects nào (fetching, timers, subscriptions, analytics). Điều này thường lộ ra các việc ẩn khiến refactor rủi ro.
Tiếp theo, yêu cầu bản đồ phụ thuộc. Bạn cần danh mục mọi input và output: props, đọc context, custom hooks, local state, giá trị phái sinh, effects, và helper ở module-level. Bản đồ hữu ích cũng chỉ ra cái nào an toàn để di chuyển (pure calculations) và cái nào “dính” (timing, DOM, network).
Rồi yêu cầu nó đề xuất các candidate để tách, với một quy tắc nghiêm ngặt: tách phần view thuần túy ra khỏi phần controller có state. Các đoạn JSX-heavy chỉ cần props là ứng viên xuất sắc để trích. Phần trộn event handlers, async calls và cập nhật state thường không nên tách ngay.
Một workflow bền trong code thực tế:
Các mốc kiểm tra là quan trọng. Yêu cầu Claude Code một kế hoạch tối thiểu nơi mỗi bước có thể commit và revert. Một mốc thực tế có thể là: “Trích \u003cTableHeader\u003e mà không thay đổi logic” trước khi đụng tới sorting state.
Ví dụ cụ thể: nếu component render customer table, điều khiển filters, và fetch data, hãy trích table markup trước (headers, rows, empty state) thành component thuần. Chỉ sau đó mới di chuyển filter state hoặc fetch effect. Trật tự này giữ bugs không bay theo JSX.
Khi tách một component lớn, rủi ro không phải là di chuyển JSX. Rủi ro là vô tình thay đổi luồng dữ liệu, timing, hoặc wiring sự kiện. Xử lý extraction như công việc copy-and-wire trước, rồi cleanup sau.
Bắt đầu bằng cách tìm ranh giới đã tồn tại trong UI, không phải trong cấu trúc file. Tìm phần bạn có thể mô tả như một “thứ” riêng: header với actions, filter bar, results list, footer với pagination.
Một bước đầu an toàn là tách presentational components thuần: props in, JSX out. Giữ chúng nhàm chán có chủ ý. Không state mới, không effect mới, không API call mới. Nếu component gốc có handler click làm ba việc, giữ handler ở parent và truyền xuống.
Ranh giới an toàn thường gồm header, list và row item, filters (chỉ inputs), footer controls (pagination, totals, bulk actions), và dialogs (mở/đóng và callbacks truyền vào).
Đặt tên quan trọng hơn bạn nghĩ. Chọn tên cụ thể như UsersTableHeader hoặc InvoiceRowActions. Tránh tên chung chung như “Utils” hay “HelperComponent” vì chúng che giấu trách nhiệm và mời gọi trộn concerns.
Chỉ giới thiệu container component khi có nhu cầu thực sự: một khối UI phải sở hữu state hoặc effects để giữ tính mạch lạc. Ngay cả khi thế, giữ nó hẹp: một container tốt chỉ sở hữu một mục đích (ví dụ “filter state”) và truyền mọi thứ khác như props.
Các component lộn xộn thường trộn ba loại dữ liệu: UI state thật sự (những gì người dùng thay đổi), dữ liệu suy diễn (có thể tính), và server state (từ mạng). Nếu bạn coi tất cả là local state, refactor sẽ rủi ro vì bạn có thể vô tình thay đổi thời điểm cập nhật.
Bắt đầu bằng cách gắn nhãn từng mẩu dữ liệu. Hỏi: người dùng có chỉnh cái này không, hay tôi có thể tính từ props, state và fetched data? Hỏi tiếp: giá trị này có được sở hữu ở đây không, hay chỉ được truyền qua?
Giá trị suy diễn không nên nằm trong useState. Di chuyển chúng vào hàm nhỏ, hoặc selector có memo khi tốn kém. Điều này giảm cập nhật state và làm hành vi dễ dự đoán hơn.
Một pattern an toàn:
useState.useMemo.Effects phá vỡ hành vi khi chúng làm quá nhiều hoặc phản ứng với dependencies sai. Hướng tới một effect cho một mục đích: một effect để sync lên localStorage, một effect để fetch, một effect để subscribe. Nếu một effect đọc nhiều giá trị, thường nó đang ẩn trách nhiệm thừa.
Nếu dùng Claude Code, yêu cầu một thay đổi nhỏ: tách một effect thành hai, hoặc chuyển một trách nhiệm vào helper. Rồi chạy characterization tests sau mỗi di chuyển.
Cẩn thận với prop drilling. Thay bằng context chỉ có ích khi nó loại bỏ wiring lặp lại và làm rõ ownership. Một dấu hiệu tốt là context đọc như khái niệm cấp ứng dụng (current user, theme, feature flags), không phải là thủ thuật cho một cây component.
Ví dụ: một bảng có thể lưu cả rows và filteredRows trong state. Giữ rows là state, tính filteredRows từ rows cộng query, và giữ code lọc trong hàm thuần để dễ test và khó phá.
Refactor thất bại thường khi bạn thay quá nhiều trước khi nhận ra. Cách sửa đơn giản: làm việc theo checkpoints nhỏ, và coi mỗi checkpoint như một release mini. Dù bạn làm trong một branch, giữ thay đổi ở cỡ PR để bạn thấy gì hỏng và vì sao.
Sau mỗi bước ý nghĩa (tách component, thay đổi luồng state), dừng lại và chứng minh bạn không đổi hành vi. Bằng chứng có thể tự động (tests) và thủ công (kiểm tra nhanh trong trình duyệt). Mục tiêu không phải hoàn hảo. Mà là phát hiện nhanh.
Một vòng checkpoint thực tế:
Nếu dùng nền tảng như Koder.ai, snapshots và rollback có thể như lan can an toàn khi bạn lặp. Bạn vẫn muốn commits bình thường, nhưng snapshots giúp khi cần so sánh phiên bản “known good” với hiện tại, hoặc khi một thử nghiệm đi lệch.
Giữ một sổ tay hành vi đơn giản trong khi làm. Nó chỉ là ghi chú ngắn những gì bạn đã kiểm tra, và ngăn bạn kiểm tra lại cùng thứ nhiều lần.
Ví dụ:
Khi có lỗi, sổ tay cho bạn biết phải kiểm tra lại gì, và các checkpoint làm việc revert rẻ tiền.
Hầu hết refactor thất bại theo những cách nhỏ, nhàm chán. UI vẫn hoạt động, nhưng một quy tắc spacing biến mất, một handler click chạy hai lần, hoặc một list mất focus khi gõ. Trợ lý có thể làm tệ hơn vì code trông sạch hơn ngay cả khi hành vi trôi dạt.
Một nguyên nhân phổ biến là thay đổi cấu trúc. Bạn tách component rồi bọc thêm một \u003cdiv\u003e, hoặc đổi một \u003cbutton\u003e thành \u003cdiv\u003e có thể click được. CSS selectors, layout, keyboard navigation, và test queries có thể đổi mà không ai để ý.
Các bẫy thường làm hỏng hành vi:
{} hoặc () => {}) có thể kích hoạt render thêm và reset state con. Chú ý các props từng ổn định.useEffect, useMemo, useCallback có thể tạo stale values hoặc vòng lặp nếu dependencies sai. Nếu effect trước kia chạy “on click”, đừng biến nó thành cái chạy “khi bất cứ thứ gì thay đổi”.Ví dụ: tách bảng và đổi keys hàng từ ID sang index mảng có thể trông ổn, nhưng phá selection khi hàng reorder. Xem “sạch” là phần thưởng. Xem “cùng hành vi” là yêu cầu.
Trước khi merge, bạn cần bằng chứng rằng refactor giữ hành vi. Tín hiệu dễ nhất là nhàm chán: mọi thứ vẫn hoạt động mà bạn không phải “sửa” tests.
Chạy kiểm tra nhanh sau thay đổi cuối:
onChange vẫn kích hoạt khi người dùng nhập, không khi mount).Kiểm tra nhanh: mở component và làm một luồng kỳ quặc, như kích hoạt lỗi, retry, rồi clear filters. Refactor thường phá transitions ngay cả khi đường chính vẫn hoạt động.
Nếu bất kỳ mục nào fail, revert thay đổi cuối và làm lại bằng bước nhỏ hơn. Thường nhanh hơn là debug một diff lớn.
Hãy tưởng tượng ProductTable làm mọi việc: fetch data, quản lý filters, điều khiển pagination, mở confirm dialog để xóa, và xử lý actions hàng như edit, duplicate, archive. Ban đầu nhỏ, nó lớn lên thành file 900 dòng.
Triệu chứng quen thuộc: state rải rác trong nhiều useState, một vài useEffect chạy theo thứ tự cụ thể, và một thay đổi “vô hại” phá pagination chỉ khi filter đang active. Mọi người ngại chạm vì nó unpredictable.
Trước khi thay cấu trúc, khoá hành vi bằng vài characterization tests. Tập trung vào thao tác người dùng, không phải state nội bộ:
Giờ bạn có thể refactor theo các commit nhỏ. Kế hoạch extraction sạch có thể là: FilterBar render controls và emit filter changes; TableView render rows và pagination; RowActions sở hữu menu action và UI confirm dialog; useProductTable hook giữ logic rối (query params, derived state, side effects).
Thứ tự quan trọng. Tách UI ngu trước (TableView, FilterBar) bằng cách truyền props qua nguyên vẹn. Để phần rủi ro lại sau cùng: di chuyển state và effects vào useProductTable. Khi làm, giữ tên prop và shape event cũ để tests tiếp tục pass. Nếu test vỡ, bạn đã tìm thấy thay đổi hành vi, không phải vấn đề style.
Nếu bạn muốn refactor React components với Claude Code an toàn mỗi lần, biến những gì bạn vừa làm thành template nhỏ có thể tái sử dụng. Mục tiêu không phải thêm process. Mà là giảm bất ngờ.
Viết playbook ngắn bạn có thể theo cho bất kỳ component nào, ngay cả khi mệt hoặc vội:
Lưu đoạn này làm snippet trong notes hoặc repo để refactor tiếp theo bắt đầu với cùng lan can an toàn.
Khi component ổn định và dễ đọc hơn, chọn bước tiếp theo dựa trên ảnh hưởng tới người dùng. Thứ tự phổ biến: accessibility trước (labels, focus, keyboard), rồi performance (memoization, render đắt), rồi dọn dẹp (types, tên, mã chết). Đừng trộn cả ba trong một PR.
Nếu bạn dùng workflow kiểu vibe-coding như Koder.ai (koder.ai), planning mode giúp bạn phác thảo bước trước khi chạm code, snapshots và rollback làm checkpoint khi lặp. Khi xong, export source giúp review diff và giữ lịch sử sạch.
Dừng refactor khi tests bao phủ hành vi bạn sợ phá, thay đổi tiếp theo là tính năng mới, hoặc bạn có ham muốn “hoàn hảo hóa.” Nếu tách một form lớn gỡ rối state và tests cover validation + submit, ship nó. Lưu các ý tưởng còn lại thành backlog ngắn cho sau.
React refactors thường thay đổi identity và timing mà bạn không để ý. Các lỗi hành vi phổ biến bao gồm:
key thay đổi.Giả định rằng thay đổi cấu trúc có thể là thay đổi hành vi cho tới khi tests chứng minh ngược lại.
Dùng một mục tiêu chặt chẽ, có thể kiểm tra được, tập trung vào cấu trúc chứ không phải "cải tiến". Ví dụ mục tiêu tốt:
Tránh các mục tiêu như “làm tốt hơn” trừ khi bạn có chỉ số cụ thể và nút thắt rõ ràng.
Xem component như một hộp đen và ghi lại những gì người dùng có thể quan sát:
Nếu ghi chú của bạn nhàm chán và cụ thể, nghĩa là chúng hữu ích.
Thêm các characterization tests mô tả chính xác component đang làm gì ngày hôm nay, ngay cả khi hành vi đó lạ. Các mục tiêu thực tế:
Assertion nên kiểm tra kết quả trên UI, không phải gọi nội bộ của hook.
Yêu cầu nó hành xử như một lập trình viên pair cẩn thận:
Đừng chấp nhận một diff kiểu “viết lại lớn”; ép lấy các thay đổi dần để bạn có thể kiểm chứng.
Bắt đầu bằng việc tách các phần trình diễn thuần túy:
Copy-and-wire trước; dọn dẹp sau. Khi UI đã tách an toàn, mới chuyển đến state/effects với các bước nhỏ.
Dùng key ổn định dựa trên identity thực sự (ví dụ ID), không dùng index của mảng.
Key bằng index đôi khi “hoạt động” cho tới khi bạn sort, filter, insert hay remove hàng — rồi React tái sử dụng sai instance và gây lỗi như:
Nếu refactor thay đổi keys, coi đó là rủi ro cao và test các trường hợp reorder.
Giữ giá trị suy diễn (derived) không nằm trong useState khi có thể tính lại từ inputs hiện có.
Một cách an toàn:
filteredRows) từ + Dùng các điểm checkpoint để mọi bước đều dễ revert:
Nếu bạn dùng Koder.ai, snapshot và rollback có thể bổ trợ cho commits thông thường khi thử nghiệm đi lệch hướng.
Dừng khi hành vi đã được khoá và mã dễ sửa hơn. Dấu hiệu tốt để dừng:
Merge refactor, rồi để các mục tiếp theo (accessibility, performance, cleanup) thành công việc riêng biệt.
rowsqueryuseMemo khi phép tính tốn kémCách này giảm sự bất ngờ khi cập nhật và làm component dễ lý giải hơn.