+67

useEffect() thì cũng đơn giản nhưng mà people make it complicated

Những ngày đầu đông gõ cửa, tiết trời Hà Nội sao mà đỏng đảnh đến thế. Sáng ra vẫn còn vấn vương những tia nắng ấm, làn gió mơn man mà chiều về đã mang theo cơn mưa nhẹ lất phất. Ấy thế mà Hà Nội vẫn có cho riêng mình sức hút lạ kì. Còn gì tuyệt vời hơn khi cùng homies ghé qua những con phố, nghe hương hoa sữa thoang thoảng, hay "cố tình" đi chậm lại chỉ để ngắm nhìn những gánh cúc họa mi một chút ❤️

Chill như vậy, âu cũng là cái cớ để chúng ta cùng ngồi lại trao đổi về một chủ đề nữa trong ReactJS đón chào chiếc giao mùa nhỉ =))

◼ Đặt vấn đề

Vẫn là anh bạn mình nhắc tới trong bài viết Yêu React chẳng cần cớ, cần hiểu rõ setState() cơ !, sau hơn năm chinh chiến dự án thì React chẳng còn là cái gì đó "xoắn quẩy" với người anh nữa. Ngay ngày hôm qua, còn phím đố mình:

- Tại sao một vài trường hợp hàm trong useEffect() vẫn chạy lại trong khi dependency không hề thay đổi?

Hmm... Không biết có bạn nào gặp trường hợp này chưa?

Trong bài viết này, chúng ta cùng tìm hiểu về useEffect() để giúp mình tìm ra câu trả lời thỏa đáng để "đá bóng" lại ông anh nhé !!!

■ Đối tượng

Bài viết chủ yếu hướng tới các bạn đã nắm được các concepts cơ bản của ReactJS nhưng trong quá trình tìm hiểu còn băn khoăn về useEffect() cũng như muốn có cái nhìn rõ nét hơn về React API này ^^

Nhấp ngụm mật ong chanh cho ấm bụng rồi cùng bắt đầu thôiii! 🍯🍯

useEffect() API

■ Tổng quan

Đó giờ ReactJS đã hỗ trợ chúng ta khai báo một component theo 2 hướng cú pháp:

  • Thông qua Function
const FunctionComponent = () => <p>Function Component</p>;
  • Thông qua Class
class ClassComponent extends React.Component {
  render() {
    return <p>Class Component</p>;
  }
}

Ở thời điểm trước đó, Function Component chỉ được xem như một kiểu Presentation component/Stateless Component do chưa được React hỗ trợ về việc quản lý state, track life cycles so với Class Component.

Cho tới 2019, Function component mới thật sự bùng nổ và bắt đầu được sử dụng rộng rãi hơn khi có thêm sức mạnh của React Hooks - một nhóm các APIs được ReactJS trình làng trong version 16.8.

useEffect() là một trong số đó. API này hỗ trợ chúng ta giải quyết một số thách thức khi làm việc với life cycle trong component.

Giờ thì hãy đi vào cú pháp của useEffect()😃))

■ Cú pháp

useEffect(callback, dependency);

useEffect() là một hàm nhận vào 2 tham số:

  • Callback:

    • Là một hàm:
      • Không nhận tham số.
      • Bên trong chứa các effects cần được xử lý; Chúng sẽ được thực thi khi component được mount ở lần render-đầu-tiênđược-gọi-lại nếu một-trong-các-giá-trị trong mảng dependency thay đổi.
      • Luôn trả về hoặc một hàm khác (gọi là cleanup) hoặc undefined.
    • Bắt buộc truyền vào
  • Dependency:

    • Là một mảng gồm các dependencies, quyết định việc có gọi lại các effects trong callback hay không:

      • Nếu không truyền gì thì mặc định, các effects sẽ được gọi lại sau mỗi lần render.
      • Nếu truyền vào một mảng rỗng [] thì effects sẽ CHỈ chạy trong lần render đầu tiên.
    • Tùy chọn truyền vào hoặc không.

Đâu đó chúng ta đã bắt gặp:

useEffect(() => {
    getPostDetail(id);          // <-- effect
    
    return () => {
        cleanPostDetail();      // <-- cleanup
    }
}, [id]);                       // <-- dependency

Theo Trang chủ ReactJS:

The Effect Hook lets you perform side effects in function components.

Này là một số use cases về các side effects thực tế chúng ta thường gặp:

  • Thêm subscriber, event listener cho element.
  • Gọi dữ liệu từ API sau khi component được render trong giai đoạn mounting.
  • Thực thi một business logic/DOM update nào đó khi state hoặc props trong component thay đổi.
  • Tiến hành dọn dẹp, unsubscribe các event listeners trước đó đã sử dụng trước khi component unmount.
  • ...

Nói như vậy, liệu rằng life cycle methods trong Class component(CC) làm được gì thì useEffect() trong Function component(FC) cũng "cân" được hết hay sao nhỉ!?!

Cùng chuyển sang phần tiếp theo để đi tìm câu trả lời!

useEffect() vs. Lifecycle methods

Nếu như bạn đã quen với các life cycle methods trong Class component, có thể hiểu rằng useEffect() là sự kết hợp giữa 3 methodscomponentDidMount(), componentDidUpdate()componentWillUnmount() 😸😸))

Chi tiết được mô tả trong bảng sau:

Class Component Function Component
componentDidMount(){ effects() } useEffect(() => { effects() }, [])
componentDidUpdate(){ effects() } useEffect(() => { effects() }, dependencies)
componentWillUnmount(){ cleanup() } useEffect(() => () => { cleanup() }, [])

Open in CodeSandbox ▷▷▷

Nếu muốn tìm hiểu thêm về phần này, bạn có thể tham khảo trên Trang chủ React hoặc nghía qua chiếc slide trong một seminar giới thiệu chung về hooks mình làm cho team gần đây nhé ^^

Giờ thì điểm qua một số vấn đề thường gặp khi làm việc với useEffect()!

■ Các vấn đề thường gặp

Như đã đề cập ở trên, thay vì:

useEffect(callback);

thì với cú pháp

useEffect(callback, dependency);

chúng ta có thể kiểm soát những lần gọi lại callback không cần thiết của component.

Các giá trị được sử dụng bên trong useEffect() và nằm trong component nên được truyền vào dependency. Nếu chưa quen, thời gian đầu chúng ta có thể nhờ tới sự hỗ trợ của Lint Tools:

We provide the exhaustive-deps ESLint rule as a part of the eslint-plugin-react-hooks package to find components that don’t handle updates consistently.

Giờ thì xem qua một ví dụ:

useEffect(() => {
    getPostDetail(id);
}, [id]);  

Này thì dễ rồi nhỉ 😸😸 getPostDetail() sẽ chạy trong lần render đầu tiên và được gọi lại nếu như giá trị id thay đổi.

Hmm...

Dựa vào đâu để React phát hiện được "sự thay đổi" (change detection)?

React sẽ so sánh id hiện tại và id ở lần render ngay trước đó.

Vậy nếu nó 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 props, state, 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 😿😿

Để 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 data, doSomething() sẽ không được gọi lại.

Theo Trang chủ React:

It is only safe to omit a function from the dependency list if nothing-in-it (or the functions called by it) references props, state, or values derived from them.

The recommended fix is to move that function inside of your effect.

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 requests, logics, UIs, helpers 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 components: ParentChild:

  • Parent là component cha của Child
  • Parent truyền 2 hàm updateAnyStatesupdateCounter 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>;
}

Cùng đoán xem điều gì sẽ xảy ra khi chúng ta click vào 2 buttons nào?

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-renders, Child nhận thấy sự thay đổi của cả updateAnyStates, updateCounter.

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]);

Tương tự với cú pháp của useEffect(), useCallback() cũng nhận vào 2 tham số: callbackdependency.

useCallback() will return a memoized version of the callback that only changes-its-identity if any of the dependencies has changed, ensuring we don't create a new instance of the function every time the parent re-renders.

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 😸😸

Thử chạy lại và cảm nhận nhé 😽😽

Source code trong các ví dụ ở các mục trên, bạn có thể vào đây tham khảo ^^

Xong rồi thì cùng đi tiếp 02 lưu ý nhỏ xíu xiu nữa nào!


■ Infinite loop

Một vòng lặp vô hạn (infinite loop) có thể được tạo ra và dẫn đến các lỗi không mong muốn trong một vài trường hợp chúng ta trigger một vài sự kiện làm component re-renders (props hoặc state thay đổi) bên trong useEffect().

Quan sát ví dụ dưới đây:

const InfiniteLoop = () => {
    const [value, setValue] = useState("");
    const [count, setCount] = useState(-1);

    useEffect(() => {
        setCount(count + 1);
        console.log("Infinite Loop is created & go on ...");
    });
 
  return (
    <input type="text" value={value} onChange={({ target }) => setValue(target.value)} />
  );
};

Khi component thay đổi giá trị trường input ⇒ kích hoạt sự kiện onChangesetValue() được gọi ⇒ Component được re-rendercallback trong useEffect() được gọi lại ⇒ setCount chạy ⇒ component lại re-rendercallback trong useEffect() được gọi lại ⇒ setCount chạy ⇒ .... 😵😵

Cứ như vậy, một vòng lặp vô hạn được tạo ra.

Hướng giải quyết thì có thể chọn cách thêm dependency vào params thứ 2 của hook này:

useEffect(() => setCount(count + 1), [value]);

Do đó, trong quá trình làm việc, chúng ta cần hiểu rõ cơ chế hoạt động của useEffect()ReactJS lifecycle để có thể nắm rõ được luồng chạy của ứng dụng ^^


■ Parent Effect vs. Child Effect

Giờ thì chúng ta có 02 components:

const ParentComponent = () => {
    useEffect(() => { console.log('Parent Component') });
    return <ChildComponent />;
}

function ChildComponent() {
    useEffect(() => { console.log('Child Component') });  
}

Khi ParentComponent được render, chuỗi Child Component sẽ được log ra trước Parent Component.

Hmm... Qua đây thì cần lưu ý gì không nhỉ?

Giả sử chúng ta cần làm chức năng Tự động thanh toán. Đoạn code xử lý này được viết trong component con sau mỗi lần render. Trong khi đó, thông tin hóa đơn (tổng chi phí, thông tin giảm giá, tổng thanh toán hay các chi tiết bắt buộc khác) lại được xử lý trong effect của component cha!?!

Như vậy thì có gì đó "chưa ổn" rồi, thanh toán không thành công 😹😹

Thông qua ví dụ này, điều mình muốn nhấn mạnh là, ngoài việc nắm rõ được thứ tự lifecycle-trong-1-component, chúng ta cũng cần lưu ý một chút về tương-quan-lifecycle-giữa-các-components để có thể xây dựng một cấu trúc components phù hợp nhaaa ❤️

■ Kết

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.

Cảm ơn các bạn đã đọc bài chia sẻ này. Tặng mình 1 upvote để có thêm động lực cho những bài viết sắp tới nhé 😺😺


Và trong thời điểm hiện tại thì...

Mặc dù thời gian này (thời điểm mình publish bài viết, 01/12/2021), Hà Nội đã nới lỏng giãn cách xã hội và việc tiêm vaccine Covid-19 cũng đã được triển khai, song, chúng ta cũng chưa thể chủ quan, hãy tiếp tục tuân thủ quy tắc 5K được Bộ Y tế khuyến cáo:

#Coronavirus #5K #BoY Te
Khẩu trang - Khử khuẩn - Khoảng cách - Không tập trung - Khai báo y tế

để có thể giữ an toàn cho bản thân và mọi người xung quanh 😺😺

Chúc các bạn ngày làm việc hiệu quả! Tiện ghé qua nhà mình chơi một chút rồi về!

■ Credits


Happy coding!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.