Hướng dẫn viết Custom Hooks trong ReactJS

Trong ReactJS, ngoài những Hooks quen thuộc như useState(), useEffect(), useRef(), … Thì React còn cho phép chúng ta tự tạo ra một custom hooks với những tính năng riêng biệt. Nếu bạn chưa biết về React Hooks thì hãy xem bài viết Những React Hooks hay dùng nhất này nhé.

Bài viết này, hãy cùng mình tìm hiểu cách để viết Custom Hooks trong ReactJS qua một ví dụ về useFetchData đơn giản nhé.

Let’s get started !

hướng dẫn viết custom hooks trong reactjs

Custom Hooks là gì ?

Từ phiên bản 16.8, React Hooks chính thức được thêm vào ReactJS. Ngoài những hooks có sẵn trong React Hooks như useState, useEffect, useCallback, … Thì React cho phép chúng ta tự định nghĩa ra những hooks khác tuỳ theo mỗi nghiệp vụ.

Những đặc điểm của một Custom Hooks:

  • Là một function, nhận input và trả output.
  • Tên của nó bắt đầu bởi use như useQuery, useColor, …
  • Khác với functional component, custom hooks trả về một dữ liệu bình thường, không phải là jsx.
  • Khác với function bình thường, custom hooks có thể sử dụng các hooks khác như useState, useRef, … và cả các custom hooks khác.

Có thể bạn không để ý, các thư viện cho ReactJS cũng cung cấp các hooks cho chúng ta dùng như useForm (React Hook Form), useMediaQuery (MUI), …

Tham khảo thêm: Building Your Own Hooks

Tại sao lại sử dụng Custom Hooks

Một vài lợi ích của custom hooks có thể kể đến như:

  • Tách biệt hoàn toàn logic với giao diện.
  • Tái sử dụng ở nhiều component khác nhau có cùng logic xử lý. Từ đó nếu logic thay đổi thì chỉ cần sửa tại một nơi duy nhất.
  • Chia sẻ dữ liệu, logic giữa các component.
  • Ẩn các đoạn code có logic phức tạp của một component, giúp component dễ đọc hơn.

Khi nào thì dùng custom hooks trong ReactJS 🤔

  • Khi một đoạn code (logic) được tái sử dụng nhiều nơi (dễ thấy khi bạn copy cả 1 đoạn code mà không cần sửa gì, trừ tham số truyền vào. Tách như cách mà bạn tách một function).
  • Khi logic quá dài và phức tạp. Bạn muốn viết nó ở 1 file khác, để component của bạn ngắn hơn và dễ đọc hơn vì không cần quan tâm đến logic của hook đó nữa.

Case Study

Giả sử mình xây dựng 1 app có 2 component sau:

  • UserCard: lấy thông tin của một user từ việc call API và hiển thị thông tin nó ra (đầy đủ thông tin)
  • UserList: lấy thông tin của một danh sách người dùng và hiển thị danh sách này ra, tuy nhiên giao diện sẽ khác nên không dùng UserCard ở trên đâu nhé (chỉ hiển thị tên và số điện thoại).

Lưu ý: mình chỉ demo tính năng custom hook, không quá chú trọng giao diện.

import React from 'react'
import UserCard from "./UserCard";
import UserList from "./UserList";

export default function App() {
  return (
    <div className="App">
      <UserCard />
      <UserList />
    </div>
  );
}

Demo: Khi không sử dụng Custom Hooks

import React, { useEffect, useState } from "react";
import axios from "axios";

function UserCard() {
  const [isLoading, setIsLoading] = useState(true);
  const [userInfo, setUserInfo] = useState(null);

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

    (async function fetchData() {
      try {
        const response = await axios.get(
          "https://5f3fda1244212d0016fed4db.mockapi.io/users/1"
        );
        if (isSubscribe && response.status === 200) {
          const { data = null } = response;
          setUserInfo(data);
        }
      } catch (error) {
        console.log(error);
      } finally {
        isSubscribe && setIsLoading(false);
      }
    })();

    return () => (isSubscribe = false);
  }, []);

  return (
    <>
      {isLoading ? (
        <div> Loading ... </div>
      ) : userInfo ? (
        <div>
          <h2>Thông tin User</h2>
          <p>Họ tên: {userInfo.fullName}</p>
          <p>Tuổi: {userInfo.age}</p>
          <p>Địa chỉ: {userInfo.address}</p>
          <p>Số điện thoại: {userInfo.phone}</p>
        </div>
      ) : (
        <div>Không thể lấy thông tin user</div>
      )}
    </>
  );
}

export default UserCard;
import React, { useEffect, useState } from "react";
import axios from "axios";

function UserList() {
  const [isLoading, setIsLoading] = useState(true);
  const [list, setList] = useState([]);

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

    (async function fetchData() {
      try {
        const response = await axios.get(
          "https://5f3fda1244212d0016fed4db.mockapi.io/users"
        );
        if (isSubscribe && response.status === 200) {
          const { data = null } = response;
          setList(data);
        }
      } catch (error) {
        console.log(error);
      } finally {
        isSubscribe && setIsLoading(false);
      }
    })();

    return () => (isSubscribe = false);
  }, []);

  return (
    <>
      {isLoading ? (
        <div> Loading ... </div>
      ) : list ? (
        <ul>
          <h1>Danh sách người dùng</h1>
          {list.map((user, index) => (
            <li key={index}>
              Họ tên: {user.fullName} - Số điện thoại: {user.phone}
            </li>
          ))}
        </ul>
      ) : (
        <div>Không thể lấy danh sách người dùng</div>
      )}
    </>
  );
}

export default UserList;

Demo: Phân tích và sử dụng Custom Hooks

Ta thấy cả 2 component trên có một logic rất tương tự nhau. Chúng đều call API để lấy dữ liệu. Lưu trạng thái loading và data vào state để cập nhật lại khi lấy dữ liệu thành công. Chỉ có điểm khác là chúng render ra UI khác nhau và khác URL khi call API.

Từ đó ta sẽ tách logic này ra thành một custom hooks là useFetchData() để tái sử dụng lại như sau:

import axios from "axios";
import React, { useEffect, useState } from "react";

function useFetchData(URL = "", params = {}) {
  const [isLoading, setIsLoading] = useState(true);
  const [data, setData] = useState(null);

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

    (async function fetchData() {
      try {
        const response = await axios.get(URL, params);
        if (isSubscribe && response.status === 200) {
          setData(response.data || null);
        }
      } catch (error) {
        console.log(error);
      } finally {
        isSubscribe && setIsLoading(false);
      }
    })();

    return () => (isSubscribe = false);
  }, []);

  return { isLoading, data };
}

export default useFetchData;

Lúc này, tại 2 component UserCard và UserList chỉ cần dùng useFetchData mà không cần quan tâm quá nhiều logic bên trong nó. Chỉ cần biết nó trả về 2 biến là isLoading và data là được.

import React from "react";
import useFetchData from "./hooks/useFetchData";

function UserCard() {
  const { isLoading, data: userInfo } = useFetchData(
    "https://5f3fda1244212d0016fed4db.mockapi.io/users/1"
  );

  return (
    <>
      {isLoading ? (
        <div> Loading ... </div>
      ) : userInfo ? (
        <div>
          <h2>Thông tin User</h2>
          <p>Họ tên: {userInfo.fullName}</p>
          <p>Tuổi: {userInfo.age}</p>
          <p>Địa chỉ: {userInfo.address}</p>
          <p>Số điện thoại: {userInfo.phone}</p>
        </div>
      ) : (
        <div>Không thể lấy thông tin user</div>
      )}
    </>
  );
}

export default UserCard;
import React from "react";
import useFetchData from "./hooks/useFetchData";

function UserList() {
  const { isLoading, data: list } = useFetchData(
    "https://5f3fda1244212d0016fed4db.mockapi.io/users"
  );

  return (
    <>
      {isLoading ? (
        <div> Loading ... </div>
      ) : list ? (
        <ul>
          <h1>Danh sách người dùng</h1>
          {list.map((user, index) => (
            <li key={index}>
              Họ tên: {user.fullName} - Số điện thoại: {user.phone}
            </li>
          ))}
        </ul>
      ) : (
        <div>Không thể lấy danh sách người dùng</div>
      )}
    </>
  );
}

export default UserList;

Demo CodeSandbox:

Tạm kết

Hi vậy là xong, rất đơn giản đúng không nào 😊. Với việc tách các logic ra để viết một Custom Hooks trong ReactJS thì code chúng ta sẽ gọn gàng và dễ bảo trì hơn rất nhiều. Mong rằng bài viết này giúp ích được mọi người.

Cảm ơn vì đã đọc bài viết 😍

Bạn có thể tham khảo thêm:

1 Comment

  1. Ha Fa 22/06/2022

Give a Comment