2 Năm Đi Làm React Chỉ Biết useState và useEffect: Bài Học Xương Máu Về Tư Duy Tối Ưu Hóa

2 Năm Đi Làm React Chỉ Biết useState và useEffect: Bài Học Xương Máu Về Tư Duy Tối Ưu Hóa

Đi làm React 2 năm trời mà chỉ biết mỗi useState và useEffect.

Nghe có vẻ khó tin, nhưng đó là câu chuyện có thật của mình.

Hồi mới đi làm, với mình, React chỉ xoay quanh hai "người bạn thân" này. Thỉnh thoảng lắm, khi cần tương tác trực tiếp với DOM, mình mới dùng đến useRef. Mình cũng không có nhu cầu tìm hiểu thêm các hooks khác. Logic của mình lúc đó rất đơn giản: "Xong việc là được rồi, website chạy đúng chức năng khách hàng yêu cầu là thành công rồi, tìm hiểu thêm làm gì cho mệt."

Và hệ quả tất yếu đã đến. Nhiều lúc ứng dụng mình làm ra chạy được, nhưng nó cứ chậm một cách khó hiểu. Mình không tài nào tìm ra lý do, và chỉ biết tặc lưỡi đổ tại: "Chắc do thư viện này nặng", "Chắc React bị lỗi ở phiên bản này"...

Chính vì tư duy "chạy được là được" và sự thiếu tò mò đó, mà trình độ và mức lương của mình cứ dậm chân tại chỗ suốt hai năm trời. Mình loay hoay trong cái vòng luẩn quẩn của chính mình, không tìm thấy điểm nào để bứt phá.

Sau một thời gian dài, mình mới nhận ra vấn đề không nằm ở React, mà nằm ở chính kiến thức nông cạn của mình. React đã cung cấp cho chúng ta một bộ công cụ cực kỳ mạnh mẽ, và việc của lập trình viên là phải biết dùng đúng công cụ cho đúng việc.

💡 Mẹo hay: Cách học tốt nhất là được học từ những chuyên gia giỏi. Nếu bạn muốn nâng cao kỹ năng React và tối ưu hiệu năng, hãy tham khảo khoá học React Nâng Cao - Chuyên sâu tối ưu hiệu năng của chị Trương Hoài - một kỹ sư phần mềm với nhiều năm kinh nghiệm làm việc tại Singapore.

Xem chi tiết khoá học

Trong bài viết này, mình muốn chia sẻ lại những kinh nghiệm xương máu rút ra được từ 2 năm "lạc lối" đó. Đây là những hooks quan trọng trong React, không chỉ để hoàn thành công việc, mà còn để tối ưu hóa hiệu năng ứng dụng, giúp bạn viết code sạch hơn, chuyên nghiệp hơn. Hy vọng những chia sẻ này sẽ có ích cho mọi người, đặc biệt là những bạn đang ở trong hoàn cảnh giống mình trước đây.

Vấn Đề Cốt Lõi: Kẻ Thù Thầm Lặng Của Hiệu Năng

Trước khi đi vào các hooks cụ thể, chúng ta cần hiểu "kẻ thù" lớn nhất của hiệu năng trong React là gì. Đó chính là sự tái re-render không cần thiết.

Khi state hoặc props của một component thay đổi, React sẽ re-render component đó và tất cả các component con của nó. Vấn đề là, nhiều khi các component con không hề nhận được dữ liệu mới, nhưng chúng vẫn bị "vạ lây" và phải render lại. Đây chính là nguồn cơn gây ra sự chậm chạp cho ứng dụng.

"Bộ ba" hooks dưới đây sẽ giúp chúng ta giải quyết triệt để vấn đề này.

1. useMemo: Khi Bạn Cần Ghi Nhớ Một Giá Trị "Đắt Đỏ"

Hãy tưởng tượng bạn có một hàm tính toán rất phức tạp, ví dụ như lọc và sắp xếp một danh sách hàng ngàn sản phẩm. Nếu bạn đặt phép tính này trực tiếp trong component, nó sẽ phải chạy lại mỗi khi component re-render, dù cho danh sách sản phẩm không hề thay đổi. Lãng phí quá phải không?

useMemo được sinh ra để giải quyết việc này.

  • Nó làm gì? useMemo sẽ "ghi nhớ" (memoize) kết quả của một phép tính. Nó chỉ tính toán lại khi một trong các "phụ thuộc" (dependencies) mà bạn chỉ định thay đổi.
  • Khi nào dùng? Khi bạn có những phép tính tốn nhiều tài nguyên (lọc/sắp xếp mảng lớn, tính toán dữ liệu phức tạp).

Ví dụ:

// TRƯỚC KHI DÙNG useMemo
function ProductList({ products, filter }) {
  // Hàm này sẽ chạy lại MỖI KHI ProductList re-render,
  // kể cả khi chỉ có một state không liên quan thay đổi.
  const visibleProducts = filterAndSortProducts(products, filter);

  return <ul>...</ul>;
}

// SAU KHI DÙNG useMemo
import { useMemo } from 'react';

function ProductList({ products, filter }) {
  // visibleProducts chỉ được tính toán lại khi `products` hoặc `filter` thay đổi.
  const visibleProducts = useMemo(() => {
    return filterAndSortProducts(products, filter);
  }, [products, filter]); // <-- Mảng phụ thuộc

  return <ul>...</ul>;
}

2. useCallback: Người Bạn Đồng Hành Của React.memo

Tương tự useMemo nhưng useCallback dùng để "ghi nhớ" một hàm thay vì một giá trị.

"Tại sao phải ghi nhớ một hàm? Nó có tốn tài nguyên đâu?" - Mình đã từng nghĩ vậy.

Vấn đề nằm ở chỗ: mỗi lần component cha re-render, mọi hàm được định nghĩa bên trong nó sẽ được tạo lại từ đầu. Điều này có nghĩa là, dù code của hàm y hệt nhau, nhưng trong mắt JavaScript, chúng là hai hàm khác nhau ở hai vùng nhớ khác nhau.

Khi bạn truyền hàm này xuống component con dưới dạng prop, component con sẽ thấy prop của nó đã "thay đổi" và buộc phải re-render, kể cả khi bạn đã dùng React.memo cho nó.

  • Nó làm gì? useCallback trả về một phiên bản được ghi nhớ của hàm, và chỉ tạo lại hàm mới khi dependencies của nó thay đổi.
  • Khi nào dùng? Khi bạn muốn truyền một hàm callback xuống một component con đã được tối ưu bằng React.memo.
import { useState, useCallback } from 'react';
import { memo } from 'react';

// Component con được tối ưu bằng React.memo
const MyButton = memo(({ onClick }) => {
  console.log("Button re-rendered!");
  return <button onClick={onClick}>Click me</button>;
});

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

  // Mỗi lần Counter re-render, handleClick sẽ là một hàm MỚI
  // const handleClick = () => {
  //   console.log("Clicked!");
  // };

  // Dùng useCallback, handleClick sẽ chỉ được tạo một lần duy nhất
  // vì mảng phụ thuộc [] là rỗng.
  const handleClick = useCallback(() => {
    console.log("Clicked!");
  }, []); // <-- Không phụ thuộc vào gì cả

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      <MyButton onClick={handleClick} />
    </div>
  );
}

Trong ví dụ trên, nếu không có useCallback, mỗi lần bạn nhấn nút "Increment", Counter sẽ re-render, tạo ra một hàm handleClick mới và MyButton sẽ bị re-render một cách vô ích. Với useCallbackMyButton sẽ không bao giờ re-render lại.

3. useReducer: Khi useState Trở Nên Quá Tải

useState rất tuyệt cho những state đơn giản. Nhưng khi logic state của bạn trở nên phức tạp (ví dụ: state tiếp theo phụ thuộc vào state trước đó, hoặc có nhiều hành động cập nhật state khác nhau), component của bạn sẽ sớm trở thành một mớ hỗn độn với hàng loạt hàm set... khác nhau.

useReducer là một giải pháp thay thế mạnh mẽ cho useState trong những trường hợp này.

  • Nó làm gì? useReducer giúp bạn gom tất cả logic cập nhật state vào một nơi duy nhất gọi là "reducer function".
  • Khi nào dùng?
    • Khi state có logic phức tạp.
    • Khi state tiếp theo phụ thuộc vào state trước đó.
    • Khi bạn muốn tối ưu hóa hiệu năng bằng cách truyền dispatch xuống các component con thay vì nhiều hàm callback.

Ví dụ một bộ đếm đơn giản:

import { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  // state là state hiện tại, dispatch là hàm để gửi "hành động"
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  );
}

Nhìn xem, logic increment và decrement đều nằm gọn trong hàm reducer, component Counter chỉ việc gọi dispatch với hành động tương ứng. Rất sạch sẽ và dễ quản lý!

Lời Kết

Nhìn lại chặng đường 2 năm đầu, mình thấy vừa tiếc nuối nhưng cũng vừa biết ơn. Tiếc vì đã lãng phí thời gian, nhưng biết ơn vì chính những "vấp ngã" đó đã cho mình bài học sâu sắc về tầm quan trọng của việc học hỏi không ngừng.

useState và useEffect là nền tảng, là cánh cửa đưa bạn vào thế giới React. Nhưng để xây dựng nên những ứng dụng vững chắc, hiệu năng cao và có thể mở rộng, bạn cần phải nắm vững toàn bộ bộ công cụ mà React cung cấp. useMemouseCallbackuseReducer và cả useContext (để tránh prop-drilling) chính là những gì phân biệt một lập trình viên "làm được việc" và một lập trình viên "làm tốt việc".

Đừng trở thành "mình" của hai năm về trước. Hãy tò mò, hãy đặt câu hỏi "Tại sao ứng dụng lại chậm?", và hãy tìm cách làm cho nó tốt hơn. Đó chính là con đường để bạn không chỉ nâng cao kỹ năng mà còn cả giá trị của bản thân trong ngành lập trình.

Bạn đã có câu chuyện tương tự chưa? Hook "ruột" giúp bạn tối ưu hóa hiệu năng là gì? Hãy chia sẻ ở phần bình luận bên dưới nhé