Tiếng Việt

Tìm hiểu cách sử dụng hiệu quả hàm dọn dẹp effect của React để ngăn chặn rò rỉ bộ nhớ và tối ưu hóa hiệu suất ứng dụng. Hướng dẫn toàn diện cho lập trình viên React.

Dọn dẹp Effect trong React: Làm chủ Kỹ thuật Ngăn chặn Rò rỉ Bộ nhớ

Hook useEffect của React là một công cụ mạnh mẽ để quản lý các tác vụ phụ (side effects) trong các component hàm của bạn. Tuy nhiên, nếu không được sử dụng đúng cách, nó có thể dẫn đến rò rỉ bộ nhớ, ảnh hưởng đến hiệu suất và sự ổn định của ứng dụng. Hướng dẫn toàn diện này sẽ đi sâu vào sự phức tạp của việc dọn dẹp effect trong React, cung cấp cho bạn kiến thức và các ví dụ thực tế để ngăn chặn rò rỉ bộ nhớ và viết các ứng dụng React mạnh mẽ hơn.

Rò rỉ Bộ nhớ là gì và Tại sao chúng lại Tệ?

Rò rỉ bộ nhớ xảy ra khi ứng dụng của bạn cấp phát bộ nhớ nhưng không giải phóng nó trở lại hệ thống khi không còn cần thiết. Theo thời gian, những khối bộ nhớ không được giải phóng này tích tụ, tiêu tốn ngày càng nhiều tài nguyên hệ thống. Trong các ứng dụng web, rò rỉ bộ nhớ có thể biểu hiện dưới dạng:

Trong React, rò rỉ bộ nhớ thường xảy ra trong các hook useEffect khi xử lý các hoạt động bất đồng bộ, đăng ký (subscriptions), hoặc các trình lắng nghe sự kiện (event listeners). Nếu các hoạt động này không được dọn dẹp đúng cách khi component bị gỡ bỏ (unmount) hoặc render lại, chúng có thể tiếp tục chạy ngầm, tiêu thụ tài nguyên và có khả năng gây ra sự cố.

Tìm hiểu về useEffect và Tác vụ phụ (Side Effects)

Trước khi đi sâu vào việc dọn dẹp effect, hãy cùng xem lại ngắn gọn mục đích của useEffect. Hook useEffect cho phép bạn thực hiện các tác vụ phụ trong các component hàm. Tác vụ phụ là các hoạt động tương tác với thế giới bên ngoài, chẳng hạn như:

Hook useEffect chấp nhận hai đối số:

  1. Một hàm chứa tác vụ phụ.
  2. Một mảng phụ thuộc (dependency array) tùy chọn.

Hàm tác vụ phụ được thực thi sau khi component render. Mảng phụ thuộc cho React biết khi nào cần chạy lại effect. Nếu mảng phụ thuộc trống ([]), effect chỉ chạy một lần sau lần render đầu tiên. Nếu mảng phụ thuộc bị bỏ qua, effect sẽ chạy sau mỗi lần render.

Tầm quan trọng của Việc Dọn dẹp Effect

Chìa khóa để ngăn chặn rò rỉ bộ nhớ trong React là dọn dẹp bất kỳ tác vụ phụ nào khi chúng không còn cần thiết. Đây là lúc hàm dọn dẹp phát huy tác dụng. Hook useEffect cho phép bạn trả về một hàm từ hàm tác vụ phụ. Hàm được trả về này chính là hàm dọn dẹp, và nó được thực thi khi component bị gỡ bỏ hoặc trước khi effect được chạy lại (do thay đổi trong các phụ thuộc).

Đây là một ví dụ cơ bản:


import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    console.log('Effect ran');

    // This is the cleanup function
    return () => {
      console.log('Cleanup ran');
    };
  }, []); // Empty dependency array: runs only once on mount

  return (
    

Count: {count}

); } export default MyComponent;

Trong ví dụ này, console.log('Effect ran') sẽ thực thi một lần khi component được gắn vào (mount). console.log('Cleanup ran') sẽ thực thi khi component bị gỡ bỏ (unmount).

Các Kịch bản Phổ biến Yêu cầu Dọn dẹp Effect

Hãy cùng khám phá một số kịch bản phổ biến mà việc dọn dẹp effect là rất quan trọng:

1. Bộ đếm thời gian (setTimeoutsetInterval)

Nếu bạn đang sử dụng bộ đếm thời gian trong hook useEffect, điều cần thiết là phải xóa chúng khi component bị gỡ bỏ. Nếu không, các bộ đếm thời gian sẽ tiếp tục kích hoạt ngay cả sau khi component đã biến mất, dẫn đến rò rỉ bộ nhớ và có khả năng gây ra lỗi. Ví dụ, hãy xem xét một công cụ chuyển đổi tiền tệ tự động cập nhật, lấy tỷ giá hối đoái theo các khoảng thời gian:


import React, { useState, useEffect } from 'react';

function CurrencyConverter() {
  const [exchangeRate, setExchangeRate] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      // Simulate fetching exchange rate from an API
      const newRate = Math.random() * 1.2;  // Example: Random rate between 0 and 1.2
      setExchangeRate(newRate);
    }, 2000); // Update every 2 seconds

    return () => {
      clearInterval(intervalId);
      console.log('Interval cleared!');
    };
  }, []);

  return (
    

Current Exchange Rate: {exchangeRate.toFixed(2)}

); } export default CurrencyConverter;

Trong ví dụ này, setInterval được sử dụng để cập nhật exchangeRate mỗi 2 giây. Hàm dọn dẹp sử dụng clearInterval để dừng interval khi component bị gỡ bỏ, ngăn bộ đếm thời gian tiếp tục chạy và gây rò rỉ bộ nhớ.

2. Trình lắng nghe sự kiện (Event Listeners)

Khi thêm các trình lắng nghe sự kiện trong hook useEffect, bạn phải xóa chúng khi component bị gỡ bỏ. Việc không làm điều này có thể dẫn đến việc nhiều trình lắng nghe sự kiện được gắn vào cùng một phần tử, gây ra hành vi không mong muốn và rò rỉ bộ nhớ. Ví dụ, hãy tưởng tượng một component lắng nghe các sự kiện thay đổi kích thước cửa sổ để điều chỉnh bố cục cho các kích thước màn hình khác nhau:


import React, { useState, useEffect } from 'react';

function ResponsiveComponent() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
      console.log('Event listener removed!');
    };
  }, []);

  return (
    

Window Width: {windowWidth}

); } export default ResponsiveComponent;

Mã này thêm một trình lắng nghe sự kiện resize vào cửa sổ. Hàm dọn dẹp sử dụng removeEventListener để xóa trình lắng nghe khi component bị gỡ bỏ, ngăn chặn rò rỉ bộ nhớ.

3. Đăng ký (Subscriptions - Websockets, RxJS Observables, v.v.)

Nếu component của bạn đăng ký một luồng dữ liệu bằng cách sử dụng websockets, RxJS Observables, hoặc các cơ chế đăng ký khác, điều quan trọng là phải hủy đăng ký khi component bị gỡ bỏ. Việc để các đăng ký hoạt động có thể dẫn đến rò rỉ bộ nhớ và lưu lượng mạng không cần thiết. Hãy xem xét một ví dụ trong đó một component đăng ký vào một nguồn cấp dữ liệu websocket để nhận báo giá cổ phiếu theo thời gian thực:


import React, { useState, useEffect } from 'react';

function StockTicker() {
  const [stockPrice, setStockPrice] = useState(0);
  const [socket, setSocket] = useState(null);

  useEffect(() => {
    // Simulate creating a WebSocket connection
    const newSocket = new WebSocket('wss://example.com/stock-feed');
    setSocket(newSocket);

    newSocket.onopen = () => {
      console.log('WebSocket connected');
    };

    newSocket.onmessage = (event) => {
      // Simulate receiving stock price data
      const price = parseFloat(event.data);
      setStockPrice(price);
    };

    newSocket.onclose = () => {
      console.log('WebSocket disconnected');
    };

    newSocket.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    return () => {
      newSocket.close();
      console.log('WebSocket closed!');
    };
  }, []);

  return (
    

Stock Price: {stockPrice}

); } export default StockTicker;

Trong kịch bản này, component thiết lập một kết nối WebSocket đến một nguồn cấp dữ liệu cổ phiếu. Hàm dọn dẹp sử dụng socket.close() để đóng kết nối khi component bị gỡ bỏ, ngăn kết nối vẫn hoạt động và gây rò rỉ bộ nhớ.

4. Lấy dữ liệu với AbortController

Khi lấy dữ liệu trong useEffect, đặc biệt là từ các API có thể mất một thời gian để phản hồi, bạn nên sử dụng AbortController để hủy yêu cầu fetch nếu component bị gỡ bỏ trước khi yêu cầu hoàn tất. Điều này ngăn chặn lưu lượng mạng không cần thiết và các lỗi tiềm ẩn do cập nhật state của component sau khi nó đã bị gỡ bỏ. Đây là một ví dụ về việc lấy dữ liệu người dùng:


import React, { useState, useEffect } from 'react';

function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/user', { signal });
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        setUser(data);
      } catch (err) {
        if (err.name === 'AbortError') {
          console.log('Fetch aborted');
        } else {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    return () => {
      controller.abort();
      console.log('Fetch aborted!');
    };
  }, []);

  if (loading) {
    return 

Loading...

; } if (error) { return

Error: {error.message}

; } return (

User Profile

Name: {user.name}

Email: {user.email}

); } export default UserProfile;

Mã này sử dụng AbortController để hủy yêu cầu fetch nếu component bị gỡ bỏ trước khi dữ liệu được truy xuất. Hàm dọn dẹp gọi controller.abort() để hủy yêu cầu.

Tìm hiểu về Mảng Phụ thuộc trong useEffect

Mảng phụ thuộc trong useEffect đóng một vai trò quan trọng trong việc xác định khi nào effect được chạy lại. Nó cũng ảnh hưởng đến hàm dọn dẹp. Điều quan trọng là phải hiểu cách các phụ thuộc hoạt động để tránh hành vi không mong muốn và đảm bảo việc dọn dẹp đúng cách.

Mảng Phụ thuộc Rỗng ([])

Khi bạn cung cấp một mảng phụ thuộc rỗng ([]), effect chỉ chạy một lần sau lần render đầu tiên. Hàm dọn dẹp sẽ chỉ chạy khi component bị gỡ bỏ. Điều này hữu ích cho các tác vụ phụ chỉ cần được thiết lập một lần, chẳng hạn như khởi tạo kết nối websocket hoặc thêm một trình lắng nghe sự kiện toàn cục.

Phụ thuộc có Giá trị

Khi bạn cung cấp một mảng phụ thuộc có giá trị, effect sẽ được chạy lại bất cứ khi nào bất kỳ giá trị nào trong mảng thay đổi. Hàm dọn dẹp được thực thi *trước khi* effect được chạy lại, cho phép bạn dọn dẹp effect trước đó trước khi thiết lập effect mới. Điều này quan trọng đối với các tác vụ phụ phụ thuộc vào các giá trị cụ thể, chẳng hạn như lấy dữ liệu dựa trên ID người dùng hoặc cập nhật DOM dựa trên state của component.

Hãy xem xét ví dụ sau:


import React, { useState, useEffect } from 'react';

function DataFetcher({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    let didCancel = false;

    const fetchData = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const result = await response.json();
        if (!didCancel) {
          setData(result);
        }
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();

    return () => {
      didCancel = true;
      console.log('Fetch cancelled!');
    };
  }, [userId]);

  return (
    
{data ?

User Data: {data.name}

:

Loading...

}
); } export default DataFetcher;

Trong ví dụ này, effect phụ thuộc vào prop userId. Effect sẽ được chạy lại bất cứ khi nào userId thay đổi. Hàm dọn dẹp đặt cờ didCancel thành true, điều này ngăn chặn việc cập nhật state nếu yêu cầu fetch hoàn tất sau khi component đã bị gỡ bỏ hoặc userId đã thay đổi. Điều này ngăn chặn cảnh báo "Can't perform a React state update on an unmounted component".

Bỏ qua Mảng Phụ thuộc (Sử dụng Thận trọng)

Nếu bạn bỏ qua mảng phụ thuộc, effect sẽ chạy sau mỗi lần render. Điều này thường không được khuyến khích vì nó có thể dẫn đến các vấn đề về hiệu suất và vòng lặp vô hạn. Tuy nhiên, có một số trường hợp hiếm hoi mà nó có thể cần thiết, chẳng hạn như khi bạn cần truy cập các giá trị mới nhất của props hoặc state trong effect mà không cần liệt kê chúng một cách rõ ràng như các phụ thuộc.

Quan trọng: Nếu bạn bỏ qua mảng phụ thuộc, bạn *phải* hết sức cẩn thận về việc dọn dẹp bất kỳ tác vụ phụ nào. Hàm dọn dẹp sẽ được thực thi trước *mỗi* lần render, điều này có thể không hiệu quả và có khả năng gây ra sự cố nếu không được xử lý đúng cách.

Các Thực hành Tốt nhất cho việc Dọn dẹp Effect

Dưới đây là một số thực hành tốt nhất cần tuân theo khi sử dụng chức năng dọn dẹp effect:

Các Công cụ để Phát hiện Rò rỉ Bộ nhớ

Một số công cụ có thể giúp bạn phát hiện rò rỉ bộ nhớ trong các ứng dụng React của mình:

Kết luận

Làm chủ việc dọn dẹp effect trong React là điều cần thiết để xây dựng các ứng dụng React mạnh mẽ, hiệu suất cao và tiết kiệm bộ nhớ. Bằng cách hiểu các nguyên tắc dọn dẹp effect và tuân theo các thực hành tốt nhất được nêu trong hướng dẫn này, bạn có thể ngăn chặn rò rỉ bộ nhớ và đảm bảo trải nghiệm người dùng mượt mà. Hãy nhớ luôn dọn dẹp các tác vụ phụ, chú ý đến các phụ thuộc và sử dụng các công cụ có sẵn để phát hiện và giải quyết bất kỳ rò rỉ bộ nhớ tiềm ẩn nào trong mã của bạn.

Bằng cách áp dụng chuyên cần các kỹ thuật này, bạn có thể nâng cao kỹ năng phát triển React của mình và tạo ra các ứng dụng không chỉ có chức năng mà còn hiệu suất và đáng tin cậy, góp phần mang lại trải nghiệm người dùng tổng thể tốt hơn cho người dùng trên toàn cầu. Cách tiếp cận chủ động này đối với việc quản lý bộ nhớ sẽ phân biệt các nhà phát triển có kinh nghiệm và đảm bảo khả năng bảo trì và mở rộng lâu dài cho các dự án React của bạn.