Giải thích đơn giản về React.useEffect ()
Tags: react, hook, tutorial
Published: 2022-05-17 13:36:07

Bài viết dịch từ:
https://dmitripavlutin.com/react-useeffect-explanation/

###1. useEffect () dành cho side-effect

Một component React sử dụng các props và/hoặc state để tính toán kết quả đầu ra. **Nếu component thực hiện các tính toán không nhắm mục đích là giá trị đầu ra, thì các tính toán này được đặt tên là side effect**.

Ví dụ về các `side effect` là lấy data, thao tác DOM trực tiếp, sử dụng các hàm hẹn giờ như setTimeout (), v.v.

**Component rendering và logic của `side effect` là độc lập. Sẽ là một sai lầm nếu thực hiện các `side effect` trực tiếp trong phần thân của Component , vốn chủ yếu được sử dụng để tính toán kết quả đầu ra**.

Tần suất hiển thị của component không phải là điều bạn có thể kiểm soát - nếu React muốn hiển thị component, bạn không thể dừng nó.

```
function Greet({ name }) {
  const message = `Hello, ${name}!`; // Calculates output
  // Bad!
  document.title = `Greetings to ${name}`; // Side-effect!
  return <div>{message}</div>;       // Calculates output
}
```

Làm thế nào để tách rendering khỏi `side effect`? Hãy dùng 
`useEffect()` - hook chạy các `side effect` độc lập với việc rendering.

```
import { useEffect } from 'react';
function Greet({ name }) {
  const message = `Hello, ${name}!`;   // Calculates output
  useEffect(() => {
    // Good!
    document.title = `Greetings to ${name}`; // Side-effect!
  }, [name]);
  return <div>{message}</div>;         // Calculates output
}
```

`useEffect()` hook chấp nhận 2 đối số:

`useEffect(callback[, dependencies]);
`

- `callback` là hàm chứa logic của `side effect`. `callback` được thực thi ngay sau khi các thay đổi được đẩy vào DOM.
- `dependencies` là một mảng tùy chọn của các `dependencies` . `useEffect()` chỉ thực thi lệnh gọi lại nếu các `dependencies` thay đổi giữa các lần hiển thị.

> Đặt logic của side effect vào hàm callback, sau đó sử dụng đối số dependencies để kiểm soát thời điểm bạn muốn side effect chạy. Đó là mục đích duy nhất của useEffect().

![Image 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22swjdpn18jt1w900zsn.PNG)

Ví dụ: trong đoạn code trước đó, bạn đã thấy `useEffect()` đang hoạt động:

```
useEffect(() => {
  document.title = `Greetings to ${name}`;
}, [name]);
```
Cập nhật tiêu đề document là `side effect` vì nó không trực tiếp tính toán kết quả đầu ra của component. Đó là lý do tại sao cập nhật tiêu đề document  được đặt trong một hàm callback và được cung cấp cho `useEffect()`.

Ngoài ra, bạn không muốn bản cập nhật tiêu đề document thực thi mỗi khi component Greet hiển thị. Bạn chỉ muốn nó được thực thi khi prop name thay đổi - đó là lý do bạn cung cấp tên làm dependency cho `useEffect (callback, [name])`.

### 2. Đối số Dependencies

Đối số `Dependencies` của `useEffect(callback, dependencies)` cho phép bạn kiểm soát thời điểm **side effect** chạy. Khi `Dependencies` là:

#### A) Không có: Side effect chạy sau mỗi lần rendering

```
import { useEffect } from 'react';
function MyComponent() {
  useEffect(() => {
    // Runs after EVERY rendering
  });  
}
```
#### B) Array rỗng []: side-effect chạy một lần sau lần hiển thị đầu tiên.

```
import { useEffect } from 'react';
function MyComponent() {
  useEffect(() => {
    // Runs ONCE after initial rendering
  }, []);
}
```

####C) Có props hoặc state [prop1, prop2, ..., state1, state2]: side-effect chỉ chạy khi bất kỳ giá trị phụ thuộc nào thay đổi.

```
import { useEffect, useState } from 'react';
function MyComponent({ prop }) {
  const [state, setState] = useState('');
  useEffect(() => {
    // Runs ONCE after initial rendering
    // and after every rendering ONLY IF `prop` or `state` changes
  }, [prop, state]);
}
```
Hãy phân tích các trường hợp B) và C) vì chúng được sử dụng thường xuyên.

### 3. Vòng đời của component

#### 3.1 ComponentDidMount

Sử dụng một mảng phụ thuộc trống để gọi một `side effect` một lần sau khi component được mount:

```
import { useEffect } from 'react';
function Greet({ name }) {
  const message = `Hello, ${name}!`;
  useEffect(() => {
    // Runs once, after mounting
    document.title = 'Greetings page';
  }, []);
  return <div>{message}</div>;
}

```
useEffect (..., []) được cung cấp một mảng trống làm đối số phụ thuộc. Khi được cấu hình theo cách như vậy, `useEffect ()` sẽ thực hiện callback chỉ một lần, sau khi được mount vào DOM.

Ngay cả khi component hiển thị với thuộc tính `name` khác, **side effect** chỉ chạy một lần sau lần hiển thị đầu tiên:

```
// First render
<Greet name="Eric" />   // Side-effect RUNS
// Second render, name prop changes
<Greet name="Stan" />   // Side-effect DOES NOT RUN
// Third render, name prop changes
<Greet name="Butters"/> // Side-effect DOES NOT RUN
```

{% codesandbox sweet-jepsen-r8m6t %}

####3.2 Component did update

Mỗi khi side effect sử dụng props hoặc state, bạn phải chỉ ra các giá trị này dưới dạng dependencies:

```
import { useEffect } from 'react';
function MyComponent({ prop }) {
  const [state, setState] = useState();
  useEffect(() => {
    // Side-effect uses `prop` and `state`
  }, [prop, state]);
  return <div>....</div>;
}

```

`useEffect (callback, [prop, state])` gọi hàm **callback** sau khi các thay đổi được mount vào DOM và khi và chỉ khi bất kỳ giá trị nào trong mảng phụ thuộc [prop, state] đã thay đổi.

Sử dụng đối số **dependencies** của `useEffect()`, bạn kiểm soát thời điểm gọi **side effect**, độc lập với các chu kỳ hiển thị của component. Một lần nữa, đó là bản chất của hook `useEffect()`.

Hãy cải thiện component **Greet** bằng cách sử dụng prop `name` trong tiêu đề của document:

```
import { useEffect } from 'react';
function Greet({ name }) {
  const message = `Hello, ${name}!`;
  useEffect(() => {
    document.title = `Greetings to ${name}`; 
  }, [name]);
  return <div>{message}</div>;
}
```

Prop name được đề cập trong đối số dependencies của `useEffect (..., [name])`. `useEffect()` hook chạy **side effect** sau lần hiển thị ban đầu và chỉ khi hiển thị sau khi giá trị `name` thay đổi.

{% codesandbox nifty-yonath-mo2qf %}

### 5, Xóa Side-effect 

Một số side-effect cần được xóa: đóng socket, xóa bộ hẹn giờ.

Nếu `callback` của `useEffect(callback, deps)`trả về một hàm, thì useEffect () coi đây là xóa side effect:

Phần xóa side effect (dọn dẹp) hoạt động theo cách sau:

A) Sau khi rendering ban đầu, `useEffect()` gọi hàm callback có side effect. **chức năng dọn dẹp không được gọi**.

B) Trong các lần hiển thị sau đó, trước khi gọi side effect của callback tiếp theo, `useEffect()` gọi hàm dọn dẹp từ lần thực thi side effect trước đó (để dọn dẹp mọi thứ sau side effect trước đó), sau đó chạy side effect hiện tại.

C) Cuối cùng, sau khi component được loại bỏ khỏi DOM `useEffect()` gọi hàm dọn dẹp từ side effect mới nhất.


![Image capture 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3esoh31mrkb3wsinehnn.PNG)

Hãy xem một ví dụ khi dọn dẹp side effect là hữu ích.

Component sau `<RepeatMessage message = "My Message" />` có props là `message`. Sau đó, cứ sau 2 giây, props `message` được ghi vào console:

```
import { useEffect } from 'react';
function RepeatMessage({ message }) {
  useEffect(() => {
    setInterval(() => {
      console.log(message);
    }, 2000);
  }, [message]);
  return <div>I'm logging to console "{message}"</div>;
}
```
{% codesandbox restless-wildflower-c0cfw %}

Ở demo nhập text. Console ghi lại 2 giây một lần bất kỳ message nào đã từng được nhập từ input. Tuy nhiên, bạn chỉ cần log ra message mới nhất.

Đó là trường hợp để dọn dẹp side effect: hủy bộ hẹn giờ trước đó khi bắt đầu bộ hẹn giờ mới. Hãy trả về một chức năng dọn dẹp dừng bộ đếm thời gian trước đó:

```
import { useEffect } from 'react';
function RepeatMessage({ message }) {
  useEffect(() => {
    const id = setInterval(() => {
      console.log(message);
    }, 2000);
    return () => {
      clearInterval(id);
    };
  }, [message]);
  return <div>I'm logging to console "{message}"</div>;
}
```
{% codesandbox gracious-tdd-gy4zo %}

Thử trên demo, nhập vào ô input chỉ message mới nhất được log ra.

###6. Ứng dụng của useEffect

####6.1 Fetching data

useEffect() có thể thực hiện side effect là lấy dữ liệu.

Component **FetchEmployees** lấy danh sách nhân viên:

```
import { useEffect, useState } from 'react';
function FetchEmployees() {
  const [employees, setEmployees] = useState([]);
  useEffect(() => {
    async function fetchEmployees() {
      const response = await fetch('/employees');
      const fetchedEmployees = await response.json(response);
      setEmployees(fetchedEmployees);
    }
    fetchEmployees();
  }, []);
  return (
    <div>
      {employees.map(name => <div>{name}</div>)}
    </div>
  );
}
```

useEffect() bắt đầu lấy data từ request bằng cách gọi hàm bất đồng bộ fetchEmployees() sau lần khởi tạo ban đầu.

Khi request hoàn tất, `setEmployees(fetchedEmployees)` sẽ cập nhật state của nhân viên với danh sách nhân viên vừa lấy được.

Lưu ý rằng đối số callback của `useEffect(callback)` không thể là một hàm bất đồng bộ. Nhưng bạn luôn có thể định nghĩa và sau đó gọi một hàm bất đồng bộ bên trong chính callback:

```
function FetchEmployees() {
  const [employees, setEmployees] = useState([]);
  useEffect(() => {  // <--- CANNOT be an async function
    async function fetchEmployees() {
      // ...
    }
    fetchEmployees(); // <--- But CAN invoke async functions
  }, []);
  // ...
}
```

Để get data từ props hoặc state, chỉ cần chỉ ra dependencies trong đối số dependencies : useEffect (fetchSideEffect, [prop, stateValue]).

###7. Kết luận

`useEffect(callback, dependencies)` là hook quản lý các **side effect** trong các component chức năng. đối số **callback** là một hàm để đặt **logic của side effect**. **dependencies** là danh sách các phụ thuộc của **side effect** của bạn: là props hoặc state.

useEffect(callback, dependencies) gọi lại sau khi gắn vào DOM lần đầu và trong các lần rendering sau đó, nếu bất kỳ giá trị nào bên trong các dependencies thay đổi.