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
useNavigatevàuseLocationvới các phương thức điều hướng có kiểu dữ liệu. - Một thành phần
Topbarvớ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/:idkhi nhấp chuột. - Một router gọn gàng sử dụng
createBrowserRouter(vớiSuspensecho 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
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:
NavOptionsgiữ 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
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
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
// 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
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
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: truecho 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
URLSearchParamskhi 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.