useEffect thường được dùng để lấy dữ liệu trong React. Việc lấy dữ liệu thường được thực thi bởi hàm bất đồng bộ, và khi đó việc sử dụng useEffect có lẽ không đơn giản như bạn nghĩ. Hãy cùng tìm hiểu nhé.

Cách sử dụng sai

// ❌ không nên dùng
useEffect(async () => {
  const data = await fetchData();
}, [fetchData])

Nếu bạn viết dòng code trên, Linter sẽ báo lỗi ngay lập tức. Vấn đề ở đây là tham số đầu tiên của useEffect phải là 1 hàm trả về undefined hoặc là 1 hàm xóa side-effects. Trong khi đó, hàm async lại trả về một promise chứ không phải là function. Vậy làm thế nào để xử lí code bất đồng bộ bên trong hook useEffect?

Cách viết hàm bất đồng bộ trong useEffect

Cách giải quyết thông thường là viết đoạn code lấy data bên trong useEffect. Đoạn code như dưới đây:

useEffect(() => {
  // khi báo hàm lấy data
  const fetchData = async () => {
    const data = await fetch('https://yourapi.com');
  }

  // gọi hàm
  fetchData()
    // bắt lỗi
    .catch(console.error);
}, [])

Nếu muốn sử dụng kết quả từ code bất đồng bộ, bạn nên xử lý bên trong hàm fetchData. Đoạn code dưới đây có bug do xử lí ngoài hàm fetchData:

useEffect(() => {
  // khi báo hàm lấy data
  const fetchData = async () => {
    // lấy data từ api
    const data = await fetch('https://yourapi.com');
    // đổi sang json
    const json = await data.json();
    return json;
  }

  // gọi hàm
  const result = fetchData()
    //bắt lỗi
    .catch(console.error);;

  // ❌ lỗi bắt đầu từ đây
  setData(result);
}, [])

Bạn có đoán được giá trí của result khi setData(result) được gọi không?
Có thể dùng hàm async và chờ 1 lúc để kiểm tra:

useEffect(() => {
  
  const fetchData = async () => {
    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }
    await sleep(1000);
    return 'Hello World';
  };

  const result = fetchData()
    .catch(console.error);;
   
  console.log(result);

Giá trị của result được log ra sẽ là 1 promise Promise {<pending>}.
Vậy chúng ta nên sử dụng kết quả của hàm bất đồng bộ bên trong useEffect như thế nào? Ngay bên trong hàm fetchData. Để fix lỗi trên, bạn có thể làm như sau:

useEffect(() => {
const fetchData = async () => {
 await sleep(1000);
 
 console.log('Hello World');
};

fetchData()
 .catch(console.error);;
}, [])

Khi dùng với hook setState:

useEffect(() => {
  const fetchData = async () => {
    const data = await fetch('https://yourapi.com')
    const json = await response.json();
    setData(json);
  }

  // call the function
  fetchData()
    .catch(console.error);;
}, [])

Nếu muốn trích xuất hàm bên ngoài useEffect thì làm sao>

Trong một số trường hợp, bạn muốn lấy dữ liệu bên ngoài useEffect. Khi đó bạn cần dùng useCallback.
Bởi vì khi hàm được khai báo bên ngoài useEffect, bạn sẽ phải đặt nó trong mảng phụ thuộc của hook. Nếu hàm không được bọc trong useCallback, nó sẽ update trong mọi lần re-render, và useEffect cũng sẽ được gọi mỗi khi re-render. Đây là điều chúng ta không muốn.

const fetchData = useCallback(async () => {
  const data = await fetch('https://yourapi.com');

  setData(data);
}, [])

// the useEffect is only there to call `fetchData` at the right time
useEffect(() => {
  fetchData()
    // make sure to catch any error
    .catch(console.error);;
}, [fetchData])

Lưu ý khi lấy dữ liệu trong useEffect

Đoạn code sau nhắc lại cách lấy dữ liệu đúng khi dùng useEffect:

useEffect(() => {
  const fetchData = async () => {
    const data = await fetch('https://yourapi.com');
    const json = await response.json();
    setData(json);
  }

  // call the function
  fetchData()
    .catch(console.error);;
}, [])

Một đọc giả nhận xét rằng đôi khi bạn cần có thể hủy việc gọi hàm setData. Nếu việc gọi api phụ thuộc vào params:

useEffect(() => {
  // declare the async data fetching function
  const fetchData = async () => {
    // get the data from the api
    const data = await fetch(`https://yourapi.com?param=${param}`);
    // convert the data to json
    const json = await response.json();

    // set state with the result
    setData(json);
  }

  // call the function
  fetchData()
    // make sure to catch any error
    .catch(console.error);;
}, [param])

Nếu params thay đổi giá trị, fetchData sẽ được gọi hai lần. Nếu việc này xảy ra nhanh chóng, sẽ có trường hợp lần gọi thứ nhất sẽ được xử lí sau lần gọi thứ hai. Và state sẽ lưu giữ giá trị của lần gọi thứ nhất.
Để giải quyết vấn đề trên, cần có một biến để quản lý việc update state.

useEffect(() => {
  let isSubscribed = true;

  // declare the async data fetching function
  const fetchData = async () => {
    // get the data from the api
    const data = await fetch(`https://yourapi.com?param=${param}`);
    // convert the data to json
    const json = await response.json();

    // set state with the result if `isSubscribed` is true
    if (isSubscribed) {
      setData(json);
    }
  }

  // call the function
  fetchData()
    // make sure to catch any error
    .catch(console.error);;

  // cancel any future `setData`
  return () => isSubscribed = false;
}, [param])

Link gốc: https://devtrium.com/posts/async-functions-useeffect#note-on-fetching-data-inside-useeffect