Các vấn đề thường gặp khi dùng useEffect trong ReactJs phần 1

useEffect là một hook trong ReactJS cho phép bạn thực hiện các tác vụ phụ thuộc vào lifecycle của component. Nó giúp bạn tương tác với DOM, gọi API, đăng ký/sẵn sàng các sự kiện, và thực hiện các tác vụ không đồng bộ khác.

Các khái niệm, ý nghĩa và cú pháp của useEffect đã quá quen thuộc với các anh em dev ReactJs nên ở bài viết này mình sẽ chỉ đề cập đến các vấn đề thường gặp phải khi dùng hook này và các lưu ý để sử dụng useEffect hiệu quả hơn.

Nào cùng vào vấn đề ngay thôi!!

1. Vòng lặp vô tận

useEffect sẽ được gọi mỗi khi component render lại. Nếu không chú ý, bạn có thể gặp phải vòng lặp vô tận nếu gọi một hàm trong useEffect mà thay đổi state và dẫn đến việc render component liên tục. Để tránh vấn đề này, bạn cần chỉ định một mảng dependencies (dependencies array) là tham số thứ hai của useEffect và đảm bảo rằng các dependencies không thay đổi trong useEffect

Dưới đây là một ví dụ về vòng lặp vô tận có thể xảy ra khi sử dụng useEffect mà không xử lý đúng dependencies:

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

const InfiniteLoopExample = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1); // Thay đổi state count trong useEffect
  });

  return (
    <div>
      <p>Count: {count}</p>
    </div>
  );
};

export default InfiniteLoopExample;

Trong ví dụ trên, useEffect không có dependencies được chỉ định. Mỗi khi component render lại, useEffect sẽ được gọi và gọi setCount để tăng giá trị của count lên 1. Tuy nhiên, điều này dẫn đến một vòng lặp vô tận, vì mỗi lần setCount được gọi, nó sẽ làm cho component render lại, và useEffect sẽ lại được gọi và thay đổi count một lần nữa, và quá trình này lặp đi lặp lại mà không bao giờ kết thúc.

Để khắc phục vòng lặp vô tận này, bạn cần chỉ định dependencies cho useEffect, đảm bảo rằng setCount chỉ được gọi khi count thay đổi. Bạn có thể chỉ định count là dependencies bằng cách truyền một mảng dependencies vào useEffect:

useEffect(() => {
  setCount(count + 1);
}, [count]); // Chỉ định dependencies là count

Khi bạn chỉ định count là dependencies, useEffect chỉ được gọi lại khi count thay đổi. Điều này ngăn chặn vòng lặp vô tận và chỉ thực hiện setCount khi count thay đổi, không phải mỗi khi component render lại.

2. Nếu giá trị trong dependencies là một Object hay Function thì sao?

 Trong JavaScript, chúng ta biết rằng khi so sánh:

// Primitive values
const prevURL = 'https://haodev.wordpress.com';
const currURL = 'https://haodev.wordpress.com';
prevURL === currURL    // TRUE

// Reference values
const prevBlog = { name: 'Make It Awesome' };
const currBlog = { name: 'Make It Awesome' };
prevBlog === currBlog  // FALSE

Ở một số trường hợp thực tế, Dependency có thể là 1 mảng các propsstate, thậm chí là một function, như vậy thì sẽ không thể tránh khỏi việc trigger effects thừa thãi.

Cùng đi vào chi tiết nhé!

Dependency chứa object:

const ObjDependency = () => {
  const [vote, setVote] = useState({
    value: 0,
  });

  useEffect(() => {
    console.log("Component is invoked when vote changes");
  }, [vote]);

  return (
    <>
      <p>Vote value: {vote.value}.</p>
      <button onClick={() => setVote({ value: 0 })}>Set vote = 0</button>
    </>
  );
};

Lúc này, dù vote.value vẫn bằng 0 nhưng chuỗi Component is invoked when vote changes vẫn sẽ được log ra khi ta click vào button. Lúc này, dù vote.value vẫn bằng 0 nhưng chuỗi Component is invoked when vote changes vẫn sẽ được log ra khi ta click vào button

Để xem nào, chúng ta sẽ có một vài hướng tiếp cận để xử lý như sau:

  • Chỉ thêm những giá trị property thật sự cần thiết
useEffect(() => {
    console.log("Component is invoked when vote.value changes");
}, [vote.value]);

Truyền vote.value vào mảng dependency thay vì đưa cả object vote vào như trước.

Song, chẳng phải lúc nào value cũng tồn tại trong vote (optional property) hoặc nếu object đó có nhiều properties thì ta phải liệt kê hết vào hay sao? 🙃🙃))

Đến đây thì có thể tham khảo 03 cách tiếp theo:

  • Sử dụng JSON.stringify()
useEffect(() => {
    console.log("Component is invoked when JSON.stringify(vote) changes");
}, [JSON.stringify(vote)]);
  • Kết hợp useRef() and một số helpers hỗ trợ so sánh object
const useDeepCompareWithRef = (value) => {
    const ref = useRef();
    // Hoặc 1 helper deep comparison 2 objects thay vì lodash _.isEqual()
    if (!_.isEqual(value, ref.current)) {
        ref.current = value;
    }
    return ref.current;
};

useEffect(() => {
     console.log("Component is invoked when vote changes with useDeepCompareWithRef()");
}, [useDeepCompareWithRef(vote)]);  
  • Dùng use-deep-compare-effect

Tới đây, nếu object của chúng ta vẫn quá phức tạp để so sánh (thông thường thì không tới mức đó, hoặc chỉ đơn giản là bạn muốn kế thừa open source sẵn có 😸😸) thì có thể tham khảo package này.

Chỉ cần thay useEffect() bằng useDeepCompareEffect() là mọi thứ ổn thỏa, chẳng cần "xoắn quẩy" nữa:

import useDeepCompareEffect from 'use-deep-compare-effect';

useDeepCompareEffect(() => {
    console.log("Component is invoked when vote changes with useDeepCompareEffect()");
},[obj])

Dependency chứa function:

Xét một trường hợp dưới đây:

const FuncDependency = ({ data}) => {
    const doSomething = () => { console.log(data); };

    useEffect(() => {
        doSomething();
    }, []);
    // ...
};

doSomething() sử dụng props data, nhưng data lại không nằm trong dependency. Điều này dẫn tới việc khi ai đó cập nhật datadoSomething() sẽ không được gọi lại.

Do đó, trong trường hợp này, chúng ta có thể định nghĩa doSomething bên trong useEffect() rồi gọi luôn.

Thực tế trong dự án, chúng ta thường tách các requestslogicsUIshelpers thành các files độc lập để thuận tiện cho việc tái sử dụng và viết unit test hoặc hàm đó là một props nhận từ component cha

Cùng đi tới một ví dụ nữa, chúng ta có 02 componentsParent và Child:

  • Parent là component cha của Child
  • Parent truyền 2 hàm updateAnyStates và updateCounter xuống Child

như đoạn code dưới đây:

const Parent = () => {
    const [counter, setCounter] = useState(0);
    const [anotherState, setAnotherState] = useState(0);

    const doSTOnAnyChange = () => { console.log("doSTOnAnyChange runs on ANY changes") };
    const doSTOnCounterChange   = () => { console.log("doSTOnCounterChange should run on COUNTER changes") };

    return (
        <>
          <button onClick={() => setCounter(counter + 1)}>Update counter state</button>
          <button onClick={() => setAnotherState(anotherState + 1)}>Update a different state</button>
          <Child doSTOnAnyChange={doSTOnAnyChange} doSTOnCounterChange={doSTOnCounterChange} />
        </>
    );
}
const Child = ({ doSTOnAnyChange, doSTOnCounterChange }) => {
    useEffect(() => {
        doSTOnAnyChange();
    }, [doSTOnAnyChange]);

    useEffect(() => {
        doSTOnCounterChange();
    }, [doSTOnCounterChange]);

  return <p>Child</p>;
}

Luôn có 2 chuỗi:

> doSTOnAnyChange runs on ANY changes
> doSTOnCounterChange should run on COUNTER changes

được log dưới cửa sổ Console, điều này nghĩa là, khi Parent re-rendersChild nhận thấy sự thay đổi của cả updateAnyStatesupdateCounter.

Thực tế thì hàm doSTOnCounterChange – đúng như tên gọi của nó – chỉ cần chạy lại khi có sự thay đổi của state couter thôi.

Tới đây thì useCallback() được sinh ra cho “đời bớt khổ”, hạn chế những lần chạy không cần thiết 😸😸

Chỉ cần thay đổi một chút khi khai báo hàm doSTOnCounterChange:

const doSTOnCounterChange = useCallback(() => {
    console.log("doSTOnCounterChange should run on COUNTER changes");
}, [counter]);

Với useCallback(), chẳng cần phải lo nghĩ doSTOnCounterChange bị tạo lại mỗi lần Parent re-renders nữa.

Như vậy là chúng ta đã cùng nhau điểm qua cơ chế hoạt động của useEffect() và một số trường hợp thú vị xung quanh nó rồi.

Hy vọng rằng bài viết này có thể giúp ích được các bạn đang tiếp cận với ReactJS, từ đó có thể hiểu về luồng của ứng dụng và kiểm soát được một số lỗi liên quan tốt hơn.

Related Posts