0
0
Lập trình
Admin Team
Admin Teamtechmely

Hướng dẫn sử dụng React Router: Tạo Hook `useRouteNav` và Điều hướng Bằng Nút

Đăng vào 7 tháng trước

• 8 phút đọc

Hướng dẫn sử dụng React Router: Tạo Hook useRouteNav và Điều hướng Bằng Nút

Trong bài viết này, chúng ta sẽ khám phá cách tạo điều hướng hiệu quả trong ứng dụng React của bạn bằng cách sử dụng một hook tái sử dụng có tên là useRouteNav. Điều này giúp cho việc điều hướng trong ứng dụng trở nên dễ dàng và có thể kiểm soát hơn. Chúng ta sẽ xây dựng một hook điều hướng tùy chỉnh, thiết lập các thành phần để điều hướng và cuối cùng là một ứng dụng mẫu hoàn chỉnh.

Mục tiêu của chúng ta

Chúng ta sẽ xây dựng:

  • Một hook tùy chỉnh bao bọc useNavigateuseLocation với các phương thức điều hướng có kiểu dữ liệu.
  • Một thành phần Topbar với nút Tạo Khách Hàng điều hướng đến /addCustomer.
  • Một trang HomePage để tìm kiếm và điều hướng.
  • Có thể: Danh sách thẻ điều hướng đến /customers/:id khi nhấp chuột.
  • Một router gọn gàng sử dụng createBrowserRouter (với Suspense cho các trang tải chậm).
  • Một stub CustomerAddPage tối thiểu để hoàn thiện quy trình.

Mô hình này giúp cho việc mở rộng dễ dàng: tất cả điều hướng của ứng dụng sẽ tập trung tại một nơi. Khi bạn thêm phân tích, quyền truy cập, hoặc kiểm toán liên quan đến điều hướng, bạn chỉ cần thực hiện một lần.

1) Hook — useRouteNav

Chúng ta sẽ bắt đầu bằng cách tạo hook useRouteNav. Hook này sẽ bao gồm các phương thức điều hướng cần thiết cho ứng dụng của chúng ta:

typescript Copy
import { useCallback } from "react";
import { useLocation, useNavigate } from "react-router-dom";

type NavOptions = {
  replace?: boolean;
  state?: unknown;
};

export function useRouteNav() {
  const navigate = useNavigate();
  const location = useLocation();

  // Điều hướng đến trang thêm khách hàng (đường dẫn tuyệt đối)
  const goToAddCustomer = useCallback(
    (opts?: NavOptions) => {
      navigate("/addCustomer", { replace: !!opts?.replace, state: opts?.state });
    },
    [navigate]
  );

  // Điều hướng đến khách hàng theo ID cụ thể
  const goToCustomerById = useCallback(
    (id: string | number, opts?: NavOptions) => {
      navigate(`/customers/${id}`, { replace: !!opts?.replace, state: opts?.state });
    },
    [navigate]
  );

  // Điều hướng đến tìm kiếm mà vẫn giữ nguyên các tham số tìm kiếm hiện tại (tùy chọn)
  const goToSearchWithQuery = useCallback(
    (query: string, opts?: NavOptions) => {
      const params = new URLSearchParams(location.search);
      params.set("q", query.trim());
      navigate({ pathname: "/search", search: params.toString() }, { replace: !!opts?.replace, state: opts?.state });
    },
    [navigate, location.search]
  );

  return {
    goToAddCustomer,
    goToCustomerById,
    goToSearchWithQuery,
  };
}

Tại sao lại cần một hook?

  • Trách nhiệm đơn lẻ: các thành phần gọi các phương thức có nghĩa (ví dụ: goToAddCustomer) thay vì mã hóa các đường dẫn.
  • Đóng gói: bạn có thể thêm phân tích, bảo vệ xác thực, logic A/B, hoặc các flag tính năng bên trong hook sau này.
  • Kiểm soát kiểu dữ liệu: NavOptions giữ cho ý định rõ ràng (replace, state).

2) Topbar với nút “Tạo Khách Hàng”

Chúng ta sẽ thêm một prop onCreateCustomer và kết nối nó với sự kiện nhấp chuột trên nút.

typescript Copy
import { useEffect, useState, type KeyboardEvent } from "react";

interface Props {
  placeholder?: string;
  onQuery: (query: string) => void;
  onCreateCustomer: () => void; // MỚI
}

export const Topbar = ({ placeholder = "Tìm kiếm", onQuery, onCreateCustomer }: Props) => {
  const [query, setQuery] = useState("");

  const handleSearch = () => onQuery(query);

  useEffect(() => {
    const timeoutId = setTimeout(() => onQuery(query), 1000);
    return () => clearTimeout(timeoutId);
  }, [query, onQuery]);

  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    if (event.key === "Enter") handleSearch();
  };

  return (
    <div className="topbar">
      <div className="title">
        <div className="logo" aria-hidden="true">💳</div>
        <h1>Khách Hàng</h1>
        <span className="pill" id="countPill">0</span>
      </div>

      <div className="controls">
        <input
          id="q"
          type="search"
          placeholder={placeholder}
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          onKeyDown={handleKeyDown}
          autoComplete="off"
        />
        <span className="spacer" />
        <button className="primary" id="searchBtn" onClick={handleSearch}>Tìm kiếm</button>
        <button className="secondary" id="addCustomerBtn" onClick={onCreateCustomer}>
          Tạo Khách Hàng
        </button>
      </div>
    </div>
  );
};

export default Topbar;

Lưu ý

Nếu bạn cũng sử dụng một SearchBar, hãy gương mẫu prop onCreateCustomer ở đó - hoặc giữ hành động tạo chỉ trong Topbar để rõ ràng hơn.

3) Kết nối vào HomePage

typescript Copy
import Topbar từ "../../../shared/components/Topbar";
import Cards từ "../../components/Cards";
import { useCustomersChargeBee } từ "../../hooks/useCustomersChargeBee";
import { useRouteNav } từ "../../../shared/hooks/useRouteNav";

const HomePage = () => {
  const { handleSearch, customers } = useCustomersChargeBee();
  const { goToAddCustomer } = useRouteNav();

  return (
    <>
      <Topbar
        placeholder="Tìm kiếm theo tên, email, công ty, id…"
        onQuery={handleSearch}
        onCreateCustomer={goToAddCustomer}
      />
      <Cards list={customers} />
    </>
  );
};

export default HomePage;

4) (Tùy chọn) Thẻ → Đường dẫn Chi tiết

typescript Copy
// src/customers/components/Cards.tsx (bản phác thảo)
import { useRouteNav } từ "../../shared/hooks/useRouteNav";
import type { List } từ "../interfaces/customer.response";

export default function Cards({ list }: { list: List[] }) {
  const { goToCustomerById } = useRouteNav();
  return (
    <div className="cards">
      {list.map((c) => (
        <article key={c.id} onClick={() => goToCustomerById(c.id)} className="card">
          <h3>{c.first_name} {c.last_name}</h3>
          <p>{c.email}</p>
        </article>
      ))}
    </div>
  );
}

5) Thiết lập Router (tải chậm + Suspense)

Sử dụng react-router-dom cho các ứng dụng trên trình duyệt và bao bọc các route tải chậm.

typescript Copy
import { createBrowserRouter } từ "react-router-dom";
import { Suspense, lazy } từ "react";

import ChargeBeeLayout từ "../customers/layouts/ChargeBeeLayout";
import AdminLayout từ "../admin/layouts/AdminLayout";
import AdminPage từ "../admin/pages/AdminPage";
import HomePage từ "../customers/pages/home/HomePage";
import CustomerPage từ "../customers/pages/customer/CustomerPage";

const SearchPage = lazy(() => import("../customers/pages/search/SearchPage"));
const CustomerAddPage = lazy(() => import("../customers/pages/customer/CustomerAddPage"));

const withSuspense = (el: JSX.Element) => <Suspense fallback={<div>Đang tải…</div>}>{el}</Suspense>;

export const appRouter = createBrowserRouter([
  {
    path: "/",
    element: <ChargeBeeLayout />,
    children: [
      { index: true, element: <HomePage /> },
      { path: "customers/:id", element: <CustomerPage /> },
      { path: "search", element: withSuspense(<SearchPage />) },
      { path: "addCustomer", element: withSuspense(<CustomerAddPage />) },
    ],
  },
  {
    path: "/admin",
    element: <AdminLayout />,
    children: [{ index: true, element: <AdminPage /> }],
  },
]);

6) CustomerAddPage tối thiểu

typescript Copy
export default function CustomerAddPage() {
  return (
    <section style={{ padding: 24 }}>
      <h1>Tạo Khách Hàng</h1>
      {/* Biểu mẫu của bạn sẽ ở đây */}
      <p>Đang đến: biểu mẫu tạo khách hàng.</p>
    </section>
  );
}

Mẹo Chuyên Nghiệp (dành cho ứng dụng sản xuất)

  • Ưu tiên các phương thức ý định (goToAddCustomer) thay vì các đường dẫn thô trong các thành phần.
    • Các thay đổi & đường dẫn sẽ trở thành các chỉnh sửa một dòng trong hook.
  • Sử dụng replace: true cho các tình huống đăng nhập/đăng xuất để tránh vòng lặp “Quay lại đăng nhập”.
  • Chuyển state để mang theo dữ liệu không phải URL (ví dụ: thông báo flash) mà không làm ô nhiễm chuỗi truy vấn.
  • Giữ nguyên/kết hợp các tham số tìm kiếm với URLSearchParams khi di chuyển giữa các trang có thể lọc.
  • Tập trung phân tích: gửi sự kiện từ bên trong hook trước/sau khi navigate.

Tóm tắt

Tính năng Mục đích
useRouteNav Tập trung logic điều hướng (dựa trên ý định, có thể kiểm tra)
goToAddCustomer Nhấp → /addCustomer
goToCustomerById Nhấp thẻ → /customers/:id
goToSearchWithQuery Điều hướng đến /search trong khi giữ nguyên các tham số tìm kiếm hiện tại
Lộ trình tải chậm + Suspense Tăng tốc độ tải ban đầu, phân chia mã sạch hơn

Mô hình này giúp các thành phần của bạn trở nên sạch sẽ, điều hướng nhất quán và bản thân bạn trong tương lai cũng sẽ hài lòng. ⚡

Câu hỏi thường gặp

1. Tại sao nên sử dụng hook useRouteNav?
Nó giúp tách biệt logic điều hướng ra khỏi các thành phần, làm cho mã dễ bảo trì và mở rộng hơn.

2. Có thể sử dụng useRouteNav với các ứng dụng lớn không?
Có, hook này được thiết kế để mở rộng và dễ dàng bảo trì cho các ứng dụng lớn hơn.

3. Làm thế nào để thêm phân tích vào điều hướng?
Bạn có thể thêm mã phân tích vào trong hook, trước hoặc sau khi gọi navigate.

Kết luận

Bằng cách sử dụng hook useRouteNav, bạn sẽ cải thiện trải nghiệm điều hướng trong ứng dụng của mình và giúp mã nguồn trở nên sạch sẽ và dễ bảo trì hơn. Hãy bắt đầu áp dụng ngay hôm nay để nâng cao chất lượng dự án React của bạn!

Nếu bạn cần một repo khởi đầu, tôi có thể tạo một mẫu TypeScript + Vite với mọi thứ trên (các route, hook, trang, CSS) và thêm một vài bài kiểm tra.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào