Sau khi làm việc với hàng chục lập trình viên và xem qua rất nhiều mã nguồn React, tôi nhận ra một mô hình lặp lại: nhiều lập trình viên viết mã chạy ổn trong quá trình phát triển, nhưng lại gây ra lỗi nhỏ và trải nghiệm người dùng khó chịu khi lên production.
Những lỗi này thường chỉ xuất hiện khi người dùng thực sự tương tác với ứng dụng, dẫn đến tỷ lệ thoát cao, mất lòng tin người dùng, thậm chí là mất doanh thu.
Trong bài viết này, tôi sẽ phân tích 10 lỗi phổ biến tôi từng thấy (và từng mắc phải), cùng với cách khắc phục thực tế. Mục tiêu: từ "chạy được trên máy tôi" đến "ai dùng cũng thấy tuyệt".
1. Không sử dụng tham số truy vấn trong URL
Lỗi gặp phải:
const [search, setSearch] = useState('');
Trạng thái chỉ được lưu trong bộ nhớ.
Vấn đề:
- Refresh lại trang sẽ mất bộ lọc.
- Không thể chia sẻ view đã lọc.
- URL không phản ánh trạng thái giao diện.
Cách khắc phục:
Dùng useSearchParams
cho các trường hợp đơn giản:
const [searchParams, setSearchParams] = useSearchParams();
const search = searchParams.get('q') || '';
Dùng nuqs
+ zod
để có trạng thái được kiểm tra kiểu:
import { useQueryState } from 'nuqs';
import { z } from 'zod'; const { q, setQuery } = useQueryState({ q: z.string().optional() }); const handleSearch = (value) => { setQuery({ q: value });
};
Trình duyệt coi URL là nguồn chân lý – UI của bạn cũng nên như vậy.
2. Tránh sử dụng thẻ <form>
Lỗi gặp phải:
- Dùng div và onClick thay vì hành vi gốc của
<form>
.
Vấn đề:
- Không bấm Enter được để gửi.
- Trình đọc màn hình thiếu ngữ nghĩa.
- Trình duyệt không kiểm tra đầu vào.
Cách khắc phục:
<form onSubmit={(e) => { e.preventDefault(); handleSubmit();
}}> <label htmlFor="name">Name:</label> <input id="name" type="text" required /> <button type="submit">Submit</button>
</form>
Thư viện như react-hook-form
vẫn dựa vào <form>
.
3. Gọi useState
quá nhiều cho từng trường riêng biệt
Lỗi gặp phải:
const [name, setName] = useState('');
const [email, setEmail] = useState('');
Vấn đề:
- Quản lý phức tạp hơn khi biểu mẫu mở rộng.
Cách khắc phục:
- Sử dụng object:
const [form, setForm] = useState({ name: '', email: '' });
const updateField = (field, value) => { setForm(prev => ({ ...prev, [field]: value }));
};
Hoặc useReducer
cho trường hợp phức tạp:
const initialState = { name: '', email: '' };
function reducer(state, action) { switch (action.type) { case 'UPDATE_FIELD': return { ...state, [action.field]: action.value }; default: return state; }
}
const [form, dispatch] = useReducer(reducer, initialState);
4. Không sử dụng trạng thái suy diễn (Derived State)
Lỗi gặp phải:
const [birthDate, setBirthDate] = useState('');
const [age, setAge] = useState(calculateAge(birthDate));
Vấn đề:
- Trạng thái bị lặp, dẫn đến lỗi sai hoặc lỗi thời.
Cách khắc phục:
const age = calculateAge(birthDate);
Chỉ dùng useMemo
cho tính toán nặng:
const sortedItems = useMemo(() => { return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
5. Dùng useMemo
không đúng cách
Lỗi gặp phải:
const memoized = useMemo(() => doSomething(input), [valueThatChangesOnEveryRender]);
Vấn đề:
- Chạy lại trên mỗi lần render – vô nghĩa.
Cách khắc phục:
Chỉ dùng useMemo
khi:
- Tính toán nặng.
- Phụ thuộc ổn định.
const expensiveResult = useMemo(() => computeExpensiveValue(data), [data]);
6. Không có trạng thái Loading, lỗi, hoặc rỗng
Lỗi gặp phải:
const { data } = useQuery(...);
return <List items={data} />;
Vấn đề:
- Không có phản hồi khi đang tải.
- Người dùng thấy màn hình trắng khi lỗi.
- SEO có thể index trang trống.
Cách khắc phục:
const { data, isLoading, isError, error } = useQuery(...);
if (isLoading) return <SkeletonList />;
if (isError) return <p>Error: {error.message}</p>;
if (!data?.length) return <p>No items found.</p>;
return <List items={data} />;
Dùng @tanstack/react-query
để đơn giản hóa việc xử lý trạng thái.
7. Bỏ qua khả năng truy cập (Accessibility)
Lỗi gặp phải:
- Dùng
<div>
thay cho<button>
, bỏ label, quênalt
cho ảnh.
Vấn đề:
- Không dùng được bàn phím để điều hướng.
- Trình đọc màn hình không hiểu được UI.
Cách khắc phục:
<label htmlFor="username">Username:</label>
<input id="username" type="text" /> <button aria-expanded={isOpen} onClick={toggle}>Toggle</button>
Thêm alt
cho ảnh, kiểm tra với bàn phím. Dùng eslint-plugin-jsx-a11y
hoặc axe
.
8. Không Debounce dữ liệu đầu vào
Lỗi gặp phải:
<input value={query} onChange={e => setQuery(e.target.value)} />
Vấn đề:
- Cập nhật đắt đỏ xảy ra trên mỗi lần gõ phím.
Cách khắc phục:
Dùng hook debounce:
function useDebounce(value, delay) { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(id); }, [value, delay]); return debounced;
}
Hoặc dùng useDeferredValue
(React 18+) để chuyển trạng thái mượt hơn.
9. Form nhiều bước bị Reset khi Quay Lại
Lỗi gặp phải:
- Mỗi bước lưu trạng thái riêng.
Vấn đề:
- Quay lại bước trước sẽ mất dữ liệu người dùng đã nhập.
Cách khắc phục:
- Lưu trạng thái ở component cha:
const [formState, setFormState] = useState({ name: '', email: '' });
Truyền state và hàm cập nhật xuống từng bước. Với ứng dụng lớn, dùng react-hook-form
hoặc zustand
.
10. Không có Skeleton hoặc Placeholder
Lỗi gặp phải:
return isLoading ? null : <ActualList items={data} />;
Vấn đề:
- Màn hình trắng gây hoang mang. Hiệu suất cảm nhận kém.
Cách khắc phục:
function SkeletonList() { return ( <div> {[...Array(3)].map((_, i) => ( <div key={i} style={{ height: '50px', background: '#e0e0e0', marginBottom: '8px' }} /> ))} </div> );
}
Skeleton giúp cung cấp phản hồi trực quan và tránh giật layout.
Lời kết Trải nghiệm người dùng kém thường bắt nguồn từ những lỗi nhỏ trong mã nguồn. Những lỗi này có thể không xuất hiện khi phát triển cục bộ, nhưng sẽ gây khó chịu cho người dùng thật.
Viết code nên bền vững với việc refresh, tương thích với URL, và thể hiện sự thấu cảm với các trường hợp đặc biệt.
Không ai cần phải hoàn hảo. Quan trọng là phải suy nghĩ thấu đáo.
Tôi cũng từng mắc những lỗi này — và tôi đã học được từ chúng.
Còn bạn thì sao? Bạn đã từng mắc lỗi nào trong số này? Hãy chia sẻ câu chuyện hoặc thắc mắc của bạn nhé, tôi rất muốn nghe!