State modeling, forms và context
Đặt state tại owner thấp nhất cần điều phối nó, mô hình hóa transition thay vì chồng boolean, và chọn đúng nơi lưu local UI, URL, server data hay shared workflow state.
1. State ownership và nguồn sự thật
Mỗi state nên có một owner rõ ràng. Colocate state với component gần nhất dùng nó; khi hai sibling phải luôn nhất quán, lift lên common parent rồi truyền value và event callback xuống. Tránh copy cùng entity vào local state, context và cache vì các bản sao sẽ lệch nhau.
| Loại state | Nơi phù hợp | Ví dụ |
|---|---|---|
| Local UI | Component/subtree gần nhất | Popover mở, draft tạm, selected tab |
| Shareable navigation | URL/search params | Filter, page, sort, selected resource |
| Server state | Framework loader hoặc query cache | Users, orders, freshness, retry |
| Shared workflow | Common owner, reducer hoặc store có scope | Multi-step checkout, editor session |
| Cross-cutting stable value | Context | Theme, locale, authenticated identity |
Nếu value có thể derive từ props/state hiện tại, tính nó trong render hoặc selector. Nếu reset phụ thuộc identity của entity, dùng key hoặc explicit action; đừng tạo Effect chỉ để giữ hai state đồng bộ.
2. Controlled và uncontrolled forms
Controlled input lấy value/checked từ state và cập nhật synchronously trong onChange. Nó phù hợp khi UI khác phụ thuộc từng keystroke, cần format/validation trực tiếp hoặc nhiều field phối hợp. Uncontrolled input để DOM giữ giá trị ban đầu qua defaultValue/defaultChecked, rồi đọc bằng FormData hoặc ref khi submit.
function ProfileForm() {
const [name, setName] = useState('');
function submit(event) {
event.preventDefault();
saveProfile({ name: name.trim() });
}
return <form onSubmit={submit}>
<label>Tên
<input value={name} onChange={e => setName(e.target.value)} />
</label>
<button>Lưu</button>
</form>;
}
'', checkbox bằng boolean; không để value bất ngờ thành null/undefined.3. Validation và submission là UX state
Phân biệt field value, touched/dirty, client error, pending, server error và success. Không hiển thị lỗi quá sớm nếu làm gián đoạn nhập liệu; nhưng lỗi khi submit phải liên kết với field, có summary phù hợp và giữ input của user. Disable toàn form trong pending chỉ khi business rule đòi hỏi; luôn chống duplicate submit ở boundary xử lý.
- Dùng semantic
label, native constraint khi phù hợp vàaria-describedbynối error text. - Server luôn validate lại; client validation chỉ cải thiện phản hồi.
- Không biến response lỗi thành chuỗi duy nhất nếu cần map lỗi về từng field.
- React form Actions và
useActionStatecó thể quản pending/result, nhưng ownership và race policy vẫn phải rõ.
4. Reducer cho transition phức tạp
useReducer phù hợp khi nhiều event tác động lên state liên quan hoặc cần tập trung invariants. Action nên mô tả điều đã xảy ra — itemAdded, submitStarted — thay vì lộ setter implementation. Reducer phải pure: không request, timer, mutation hoặc sinh ID ngẫu nhiên bên trong.
function reducer(state, action) {
switch (action.type) {
case 'submitted':
if (!state.canSubmit) return state;
return { ...state, status: 'pending', error: null };
case 'failed':
return { ...state, status: 'error', error: action.error };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
Reducer không tự loại bỏ impossible states nếu model vẫn là nhiều boolean độc lập. Dùng status/discriminated union hoặc state machine để một transition tạo ra state hợp lệ. Unit test transition table, unknown action policy và invariant; integration test effect thật nằm ngoài reducer.
5. Context: transport, không phải state architecture
Context truyền value xuyên qua tree mà không prop-drill từng tầng. Khi provider nhận value mới theo so sánh Object.is, consumer đọc context đó re-render; memo không chặn consumer nhận context mới. Vì vậy, object/function tạo mới vô ý ở provider có thể mở rộng phạm vi cập nhật.
Split context theo change frequency và responsibility, ví dụ state context riêng dispatch context. Memoize provider value chỉ khi inputs ổn định và profiler chứng minh cần thiết. Context phù hợp cho dependency có scope; nó không tự cung cấp selector, normalized cache, persistence, devtools hay concurrency policy như một state library chuyên dụng.
const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);
function TodosProvider({ children }) {
const [todos, dispatch] = useReducer(todosReducer, []);
return <TodosContext value={todos}>
<TodosDispatchContext value={dispatch}>{children}</TodosDispatchContext>
</TodosContext>;
}
6. Decision và kiểm thử
Bắt đầu bằng local state, lift khi có coordination, encode navigation vào URL, giữ remote data trong server cache, rồi mới chọn context/store cho shared client workflow. Kiểm thử reducer độc lập, form bằng hành vi user, URL bằng back/forward/reload, và context bằng consumer thực thay vì assert implementation.