Hướng dẫn hoàn chỉnh để sử dụngRef () và Refs trong React
Tags: react, hook, tutorial
Published: 2022-05-15 11:40:52

Bài viết dịch từ trang: 
https://dmitripavlutin.com/react-useref-guide/

Trong bài đăng này, bạn sẽ học cách sử dụng hook `React.useRef ()` để tạo các giá trị có thể thay đổi liên tục (còn được gọi là tham chiếu hoặc `refs`), cũng như truy cập các phần tử DOM.

### 1. Mutable values

`useRef` (initialValue) là một hook React tích hợp sẵn chấp nhận một đối số làm giá trị ban đầu và trả về một tham chiếu (còn gọi là ref). Ref là một đối tượng có thuộc tính đặc biệt là `current`.

```
import { useRef } from 'react';
function MyComponent() {
  const reference = useRef(initialValue);
  const someHandler = () => {
    // Access reference value:
    const value = reference.current;
    // Update reference value:
    reference.current = newValue;
  };
  // ...
}
```
`reference.current` truy cập vào giá trị tham chiếu và `reference.current = newValue` cập nhật giá trị tham chiếu. Khá đơn giản.

Có 2 quy tắc cần nhớ về references:

1. Giá trị của tham chiếu được duy trì (giữ nguyên) giữa các lần re-rendering component;
2. Cập nhật tham chiếu **không làm re-rendering component**.

Bây giờ, chúng ta hãy xem cách sử dụng `useRef ()` trong thực tế.

####1.1 Trường hợp sử dụng: ghi nhật ký button khi click

Component `LogButtonClicks` sử dụng một tham chiếu để lưu trữ số lần ấn vào button:

```
import { useRef } from 'react';
function LogButtonClicks() {
  const countRef = useRef(0);
  
  const handle = () => {
    countRef.current++;
    console.log(`Clicked ${countRef.current} times`);
  };
  console.log('I rendered!');
  return <button onClick={handle}>Click me</button>;
}
```
{% codesandbox logging-button-clicks-reference-ogcnc %}

`const countRef = useRef (0)` tạo một tham chiếu countRef được khởi tạo bằng 0.

Khi button được ấn, hàm xử lý được gọi và giá trị tham chiếu được tăng lên: `countRef.current ++`. Giá trị tham chiếu được ghi vào console.

Cập nhật giá trị tham chiếu `countRef.current ++` không làm component re-rendering. Điều này được chứng minh bằng thực tế là **'I rendered!'** được ghi vào console chỉ một lần, ở lần hiển thị đầu tiên và không có render nào xảy ra khi tham chiếu được cập nhật.

Bây giờ có một câu hỏi: sự khác biệt chính giữa tham chiếu và state là gì?

### Khác biệt giữa tham chiếu và state

Hãy sử dụng lại component `LogButtonClicks` từ phần trước, nhưng lần này sử dụng hook `useState ()` để đếm số lần ấn vào button:

```
import { useState } from 'react';
function LogButtonClicks() {
  const [count, setCount] = useState(0);
  
  const handle = () => {
    const updatedCount = count + 1;
    console.log(`Clicked ${updatedCount} times`);
    setCount(updatedCount);
  };
  console.log('I rendered!');
  return <button onClick={handle}>Click me</button>;
}

```

{% codesandbox logging-button-clicks-state-nzzuk %}

Ở link trên ấn vào button. Mỗi lần bạn ấn vào, bạn sẽ thấy trong console có thông báo **'I rendered!'** - nghĩa là mỗi khi state được cập nhật, component sẽ re-render.

Vì vậy, 2 điểm khác biệt chính giữa tham chiếu và trạng thái:

1. Cập nhật tham chiếu không làm re-render, trong khi cập nhật state làm cho component re-render;

2. Cập nhật tham chiếu là đồng bộ (giá trị tham chiếu được cập nhật có sẵn ngay lập tức), trong khi cập nhật state là không đồng bộ (state được cập nhật sau khi re-render).

Từ một quan điểm cao hơn, các tham chiếu lưu trữ dữ liệu cơ sở hạ tầng về side-effect, trong khi state lưu trữ thông tin được hiển thị trực tiếp trên màn hình.

####1.2 Trường hợp sử dụng: triển khai đồng hồ bấm giờ

Bạn có thể lưu trữ bên trong một cơ sở hạ tầng dữ liệu tham khảo về các side-effect. Ví dụ: bạn có thể lưu trữ vào các con trỏ tham chiếu: timer ids, socket ids, etc.

Component `Stopwatch` sử dụng chức năng hẹn giờ `setInterval `(gọi lại, thời gian) để tăng số đếm của đồng hồ bấm giờ mỗi giây. Id bộ hẹn giờ được lưu trữ trong `timerIdRef`:

```
import { useRef, useState, useEffect } from 'react';
function Stopwatch() {
  const timerIdRef = useRef(0);
  const [count, setCount] = useState(0);
  const startHandler = () => {
    if (timerIdRef.current) { return; }
    timerIdRef.current = setInterval(() => setCount(c => c+1), 1000);
  };
  const stopHandler = () => {
    clearInterval(timerIdRef.current);
    timerIdRef.current = 0;
  };
  useEffect(() => {
    return () => clearInterval(timerIdRef.current);
  }, []);
  return (
    <div>
      <div>Timer: {count}s</div>
      <div>
        <button onClick={startHandler}>Start</button>
        <button onClick={stopHandler}>Stop</button>
      </div>
    </div>
  );
}
```

{% codesandbox stopwatch-cm7zz %}

`startHandler ()`, được gọi khi ấn vào button Start, khởi động bộ hẹn giờ và lưu id bộ hẹn giờ trong tham chiếu `timerIdRef.current = setInterval (...)`.

Để dừng đồng hồ bấm giờ, người dùng ấn vào button Stop. Trình xử lý button Stop `stopHandler ()` truy cập id bộ hẹn giờ từ tham chiếu và dừng bộ hẹn giờ `clearInterval(timerIdRef.current)`.

Ngoài ra, nếu component unmount khi đồng hồ bấm giờ đang hoạt động, chức năng dọn dẹp của `useEffect ()` cũng sẽ dừng đồng hồ bấm giờ.

Trong ví dụ về đồng hồ bấm giờ, tham chiếu được sử dụng để lưu trữ dữ liệu cơ sở hạ tầng - id bộ hẹn giờ hoạt động.

### 2. Truy cập các phần tử DOM

Một ứng dụng hữu ích khác của hook `useRef ()` là truy cập các phần tử DOM. Điều này được thực hiện trong 3 bước:

1. Xác định tham chiếu để truy cập phần tử `const elementRef = useRef ()`;
2. Gán tham chiếu đến thuộc tính ref của phần tử: `<div ref = {elementRef}> </div>`;
3. Sau khi gắn kết, `elementRef.current` trỏ đến phần tử DOM.

```
import { useRef, useEffect } from 'react';
function AccessingElement() {
  const elementRef = useRef();
   useEffect(() => {
    const divElement = elementRef.current;
    console.log(divElement); // logs <div>I'm an element</div>
  }, []);
  return (
    <div ref={elementRef}>
      I'm an element
    </div>
  );
}
```
{% codesandbox access-dom-element-hrh78 %}

2.1 Trường hợp sử dụng: focus vào input

Ví dụ, bạn sẽ cần truy cập các phần tử DOM để focus vào input  khi component mount.

Để làm cho nó hoạt động, bạn sẽ cần tạo một tham chiếu đến input, gán tham chiếu cho thuộc tính `ref` và sau khi gắn kết, hãy gọi phương thức đặc biệt `element.focus ()` trên phần tử.

Đây là cách triển khai có thể có của component `<InputFocus>`:

```
import { useRef, useEffect } from 'react';
function InputFocus() {
  const inputRef = useRef();
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  return (
    <input 
      ref={inputRef} 
      type="text" 
    />
  );
}
```

{% codesandbox input-focus-zntci %}

`const inputRef = useRef ()` tạo một tham chiếu để giữ phần tử `input`.

`inputRef` sau đó được gán cho thuộc tính `ref` của input: `<input ref = {inputRef} type = "text" />.`

React sau đó, sau khi mounting, đặt `inputRef.current` là phần tử input. Bây giờ bạn có thể đặt focus qua: `inputRef.current.focus ()`.

### Tham chiếu không có giá trị khi hiển thị ban đầu

Trong quá trình hiển thị ban đầu, tham chiếu được cho là giữ phần tử DOM là rỗng:

```
import { useRef, useEffect } from 'react';
function InputFocus() {
  const inputRef = useRef();
  useEffect(() => {
    // Logs `HTMLInputElement` 
    console.log(inputRef.current);
    inputRef.current.focus();
  }, []);
  // Logs `undefined` during initial rendering
  console.log(inputRef.current);
  return <input ref={inputRef} type="text" />;
}
```
{% codesandbox empty-on-initial-rendering-5my4g %}

Trong quá trình hiển thị ban đầu, React vẫn xác định đâu là đầu ra của component, vì vậy chưa có cấu trúc DOM nào được tạo. Đó là lý do tại sao `inputRef.current` đánh giá là không xác định trong quá trình hiển thị ban đầu.

`useEffect (callback, [])` hook gọi lại ngay sau khi mount, khi phần tử input đã được tạo trong DOM.

hàm callback của `useEffect (callback, [])` là nơi thích hợp để truy cập inputRef.current vì nó được đảm bảo rằng DOM được xây dựng.

###3. Hạn chế cập nhật refs

Phạm vi chức năng của component chức năng nên tính toán đầu ra hoặc gọi các hook.

Đó là lý do tại sao việc cập nhật một tham chiếu (cũng như cập nhật state) không nên được thực hiện bên trong phạm vi ngay lập tức của chức năng của component.

Tham chiếu phải được cập nhật bên trong callback `useEffect ()` hoặc bên trong các handlers (event handlers, timer handlers, etc).

```
import { useRef, useEffect } from 'react';
function MyComponent({ prop }) {
  const myRef = useRef(0);
  useEffect(() => {
    myRef.current++; // Good!
    setTimeout(() => {
      myRef.current++; // Good!
    }, 1000);
  }, []);
  const handler = () => {
    myRef.current++; // Good!
  };
  myRef.current++; // Bad!
  if (prop) {
    myRef.current++; // Bad!
  }
  return <button onClick={handler}>My button</button>;
}
```
###4. Tóm tắt

useRef () hook tạo ra các tham chiếu.

Gọi `const reference = useRef(initialValue)` với `initialValue` trả về một đối tượng đặc biệt có tên là `tham chiếu`. Đối tượng tham chiếu có thuộc tính `current`: bạn có thể sử dụng thuộc tính này để đọc giá trị tham chiếu `reference.current` hoặc cập nhật tham chiếu `reference.current = newValue`.

Giữa các lần component re-rendering, giá trị của tham chiếu là không đổi.

Cập nhật tham chiếu, trái với cập nhật state, không làm component re-rendering.

Ref cũng có thể truy cập các phần tử DOM. Gán tham chiếu đến thuộc tính `ref` của phần tử bạn muốn truy cập: `<div ref = {reference}> Element</div>` - và phần tử sẽ có tại `reference.current`.