+1

5 Lỗi phổ biến trong React không thể bỏ qua

Vì React là thư viện được sử dụng rộng rãi nhất hiện nay, nên những lỗi nhỏ này nếu không được chú ý có thể ảnh hưởng tới hiệu suất, khả năng bảo trì và khả năng mở rộng của ứng dụng.

Dưới đây là một số ví dụ về lỗi phổ biến trong React và cách khắc phục, các bạn hãy cùng với tôi khám phá ngay nhé!

1. Dùng chỉ số Array làm key trong danh sách thay vì ID duy nhất

Mặc dù đã được nhắc đến rất nhiều lần, nhưng đây vẫn là lỗi số 1 mà mình thấy trong các repo React.

Sử dụng chỉ số Array (index) làm key cho danh sách trong React là một cách làm tắt, dễ dẫn đến lỗi về thứ tự và gây cập nhật không hiệu quả. Thuật toán diff của React dựa vào key ổn định; nếu dùng index, React dễ bị "nhầm" và dẫn đến hành vi UI không mong muốn, tái render nhiều lần không cần thiết.

Ví dụ dưới đây, khi di chuyển hoặc xóa phần tử trong danh sách sẽ khiến React bị sai sót:

// 🚫 Before: Index as key (bad practice)
function BadList() {
  const fruits = ['Apple', 'Banana', 'Cherry'];
  return (
    <ul>
      {fruits.map((fruit, index) => (
        <li key={index}>{fruit}</li>  {/* Index used as key */}
      ))}
    </ul>
  );
}

Giải pháp: Hãy sử dụng một ID duy nhất cho mỗi phần tử:

// ✅ After: Unique ID as key (good practice)
function GoodList() {
  const fruits = [
    { id: 'a1', name: 'Apple' },
    { id: 'b2', name: 'Banana' },
    { id: 'c3', name: 'Cherry' }
  ];

  return (
    <ul>
      {fruits.map(fruit => (
        <li key={fruit.id}>{fruit.name}</li>  {/* Stable unique key */}
      ))}
    </ul>
  );
}

2. Tính toán trạng thái bằng useEffect thay vì tính trực tiếp

Đoạn code dưới đây lọc danh sách dựa trên từ khóa tìm kiếm, nhưng lại sử dụng useEffectuseState để lưu kết quả lọc. Điều này không cần thiết, làm code phức tạp hơn và dễ gây lỗi timing.

// 🚫 Before: Unnecessary effect and state for derived data
function FruitList() {
  const [searchTerm, setSearchTerm] = useState('');
  const [filteredFruits, setFilteredFruits] = useState(fruits);

  useEffect(() => {
    // Filter list whenever searchTerm changes
    setFilteredFruits(
      fruits.filter(fruit =>
        fruit.toLowerCase().includes(searchTerm.toLowerCase())
      )
    );
  }, [searchTerm]);

  return (
    <>
      <input 
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {filteredFruits.map(item => <li key={item}>{item}</li>)}
      </ul>
    </>
  );
}

Giải pháp: Tính toán danh sách lọc trực tiếp trong quá trình render, không cần thêm state mới:

// ✅ After: Derive filtered list directly, no extra state
function FruitList() {
  const [searchTerm, setSearchTerm] = useState('');
  const filteredFruits = fruits.filter(fruit =>
    fruit.toLowerCase().includes(searchTerm.toLowerCase())
  );

  return (
    <>
      <input 
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {filteredFruits.map(item => <li key={item}>{item}</li>)}
      </ul>
    </>
  );
}

3. Callback không được memo hóa gây render lại vs dùng useCallback

Trong ví dụ sau, component cha truyền một callback inline cho component con. Mỗi lần cha render lại (ví dụ khi count thay đổi), một function mới được tạo ra, khiến component con bị render lại dù được React.memo bọc.

// 🚫 Before: New handleClick function on every render
const Child = React.memo(({ onClick }) => (
  <button onClick={onClick}>Click me</button>
));

function Parent() {
  const [count, setCount] = useState(0);
  // This function is re-created on every Parent render:
  const handleClick = () => {
    console.log('Clicked');
  };

  return (
    <div>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Giải pháp: Dùng useCallback để đảm bảo hàm callback có tham chiếu ổn định:

// ✅ After: useCallback to preserve function identity
const Child = React.memo(({ onClick }) => (
  <button onClick={onClick}>Click me</button>
));

function Parent() {
  const [count, setCount] = useState(0);

  // Memoize handleClick so it isn't recreated each render:
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);  // no dependencies -> same function every time

  return (
    <div>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

4. Component đơn khối vs Kiến trúc component mô-đun

Component dưới đây vừa fetch dữ liệu vừa render UI, làm vi phạm nguyên tắc phân tách trách nhiệm. Sớm muộn gì file này cũng sẽ phình to thành 3000+ dòng, rất khó quản lý.

// 🚫 Before: Single component doing data-fetching and UI
function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetchUsers().then(data => {
      setUsers(data);
    });
  }, []);

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users.map(user => (
          <UserItem user={user} key={user.id} />
        ))}
      </ul>
    </div>
  );
}

Giải pháp: Tách ra thành container component (xử lý logic) và presentational component (render UI):

// ✅ After: Separate container (data logic) and presentational (UI) components
function UserListContainer() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetchUsers().then(data => {
      setUsers(data);
    });
  }, []);

  return <UserList users={users} />;  // hand off data to UI component
}

function UserList({ users }) {
  return (
    <div>
      <h1>User List</h1>
      <ul>
        {users.map(user => (
          <UserItem user={user} key={user.id} />
        ))}
      </ul>
    </div>
  );
}

5. Bỏ qua lỗi trong các lệnh async vs Xử lý lỗi đúng cách

Dưới đây là đoạn useEffect fetch dữ liệu nhưng không hề xử lý lỗi. Nếu fetch thất bại, UI sẽ không phản hồi gì cả:

// 🚫 Before: No error handling for fetch
useEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(data => setData(data));
    // .catch(...) is missing – errors are ignored
}, []);

Giải pháp: Luôn bắt lỗi bằng try/catch:

// ✅ After: Handle errors with try/catch and state
useEffect(() => {
  const fetchData = async () => {
    try {
      const res = await fetch('/api/data');

      if (!res.ok) {
        throw new Error(`Request failed with ${res.status}`);
      }

      const data = await res.json();
      setData(data);
    } catch (err) {
      setError(err);  // capture the error in state
    }
  };

  fetchData();
}, []);

Kết luận

Những lỗi này nếu chỉ xảy ra riêng lẻ thì có thể không phá hỏng ứng dụng ngay lập tức, nhưng khi cộng dồn lại sẽ tạo ra khoản nợ kỹ thuật rất lớn. Sửa sớm sẽ giúp bạn tiết kiệm nhiều thời gian về sau và xây dựng được một codebase thật sự đáng để mở rộng.

Cảm ơn các bạn đã theo dõi!


All rights reserved

Bình luận

Đang tải thêm bình luận...
Avatar
+1
Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí