React Router vs Reach Router: Những Khác Biệt Quan Trọng Cần Biết (React Roadmap)

Xin chào các bạn đồng nghiệp tương lai! Chào mừng trở lại với series “React Roadmap” của chúng ta. Sau khi đã cùng nhau đi qua các khái niệm cốt lõi như React là gì, Component, JSX, Props và State, Hooks cơ bản và nâng cao (useCallback, useMemo, useRef), hay các kỹ thuật như Conditional Rendering, Component Composition, và Làm việc với Danh sách, chúng ta đang xây dựng nền tảng vững chắc để tạo ra các ứng dụng React phức tạp hơn.

Một khía cạnh không thể thiếu khi xây dựng ứng dụng web hiện đại, đặc biệt là các Ứng dụng Một trang (Single Page Applications – SPAs) mà React thường được dùng để xây dựng, chính là định tuyến (routing). Định tuyến cho phép người dùng di chuyển giữa các “trang” khác nhau trong ứng dụng của bạn mà không cần tải lại toàn bộ trang web. Điều này mang lại trải nghiệm người dùng mượt mà và nhanh chóng.

Trong hệ sinh thái React, React Router từ lâu đã là giải pháp định tuyến “tiêu chuẩn vàng”. Tuy nhiên, có một thư viện định tuyến khác cũng rất phổ biến và có ảnh hưởng lớn, đó là Reach Router. Mặc dù hiện tại Reach Router đã được sáp nhập (merge) vào React Router phiên bản 6 trở đi, việc hiểu về Reach Router và những khác biệt cốt lõi của nó so với các phiên bản cũ của React Router là vô cùng quan trọng. Nó giúp chúng ta hiểu rõ hơn về cách định tuyến trong React đã phát triển như thế nào và tại sao React Router v6 lại có API như hiện tại.

Trong bài viết này, chúng ta sẽ cùng nhau mổ xẻ sự khác biệt giữa React Router (các phiên bản trước v6) và Reach Router. Chúng ta sẽ xem xét cách chúng xử lý việc khớp đường dẫn (path matching), định tuyến lồng nhau (nested routing), quản lý liên kết (links) và đặc biệt là khía cạnh trợ năng (accessibility). Cuối cùng, chúng ta sẽ thấy Reach Router đã đóng góp như thế nào vào sự phát triển của React Router v6.

Hãy cùng bắt đầu hành trình khám phá sự đối đầu (và sáp nhập) thú vị này nhé!

Định Tuyến (Routing) trong React: Tại Sao Cần Thiết?

Trước khi đi sâu vào so sánh, hãy nhắc lại nhanh về vai trò của định tuyến trong SPA. Thông thường, khi bạn click vào một liên kết trên website truyền thống, trình duyệt sẽ yêu cầu tải một trang HTML mới từ server. Với SPA, toàn bộ ứng dụng (hoặc phần lớn) được tải ban đầu. Khi bạn điều hướng (navigate), JavaScript sẽ tự động thay đổi nội dung hiển thị trên trang mà không cần tải lại. URL trên thanh địa chỉ của trình duyệt cũng được cập nhật để phản ánh trạng thái hiện tại của ứng dụng, cho phép người dùng bookmark hoặc chia sẻ các đường dẫn cụ thể.

Các thư viện định tuyến như React Router và Reach Router cung cấp các Component và API giúp bạn quản lý quá trình này một cách hiệu quả:

  • Khớp đường dẫn (Path Matching): Dựa vào URL hiện tại, thư viện định tuyến xác định Component nào cần được hiển thị.
  • Điều hướng (Navigation): Cung cấp cách thức để người dùng chuyển đổi giữa các đường dẫn (thường thông qua các liên kết hoặc lập trình).
  • Quản lý lịch sử trình duyệt (Browser History): Đồng bộ hóa URL với trạng thái ứng dụng.
  • Truyền tham số (Parameter Passing): Cho phép truyền dữ liệu qua URL (ví dụ: /users/123, trong đó 123 là ID người dùng).

Việc chọn một thư viện định tuyến tốt là rất quan trọng vì nó ảnh hưởng đến cấu trúc code, trải nghiệm người dùng và cả hiệu suất ứng dụng.

React Router (Trước Phiên Bản 6): Thư Viện “Tiêu Chuẩn” Một Thời

React Router đã thống trị không gian định tuyến trong React trong nhiều năm. Các phiên bản 4 và 5 là những phiên bản phổ biến nhất trước khi v6 ra đời. API của React Router trước v6 xoay quanh các Component chính:

  • <BrowserRouter> (hoặc <HashRouter>): Bao bọc ứng dụng của bạn để kích hoạt tính năng định tuyến dựa trên History API hoặc Hash.
  • <Route>: Component dùng để định nghĩa một route, chỉ định đường dẫn (path) và Component cần render khi đường dẫn đó khớp.
  • <Switch>: Bao bọc nhiều <Route>, chỉ render Component của <Route> đầu tiên khớp với đường dẫn hiện tại.
  • <Link>: Component dùng để tạo liên kết điều hướng.
  • useHistory, useLocation, useParams (trong React Router v5): Các Hook để truy cập thông tin định tuyến trong Functional Component (tham khảo bài về useState và useEffect để hiểu hơn về Hook).

Cách React Router trước v6 khớp đường dẫn khá đơn giản: nó kiểm tra xem đường dẫn hiện tại có bắt đầu bằng thuộc tính path của <Route> hay không. Để khớp chính xác toàn bộ đường dẫn, bạn cần thêm thuộc tính exact.

import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Trang chủ</Link>
        <Link to="/about">Giới thiệu</Link>
        <Link to="/users">Người dùng</Link>
      </nav>

      <Switch>
        {/* Phải dùng exact để chỉ khớp "/" chứ không phải "/about" hay "/users" */}
        <Route path="/" exact>
          <HomePage />
        </Route>
        <Route path="/about">
          <AboutPage />
        </Route>
        {/* Nested routes cần cấu trúc Route bên trong Component được render */}
        <Route path="/users">
          <UsersPage /> {/* UsersPage sẽ chứa Switch/Route con */}
        </Route>
        <Route>
          <NotFoundPage />
        </Route>
      </Switch>
    </BrowserRouter>
  );
}

function UsersPage() {
  // Giả sử đường dẫn hiện tại là "/users/123"
  // Để khớp route con, UsersPage cần có Switch/Route riêng
  return (
    <div>
      <h2>Trang Người Dùng</h2>
      <Switch>
        <Route path="/users/:userId">
          <UserProfile />
        </Route>
        <Route path="/users" exact>
          <UserList />
        </Route>
      </Switch>
    </div>
  );
}

Điều này dẫn đến một số vấn đề, đặc biệt là khi xử lý định tuyến lồng nhau phức tạp. Bạn thường phải lặp lại các đoạn đường dẫn và cấu trúc <Switch>/<Route> ở nhiều cấp độ Component.

Reach Router: Đối Thủ Nổi Bật Với Triết Lý Đơn Giản

Reach Router, được tạo ra bởi Ryan Florence (một trong những tác giả của React Router), ra đời với mục tiêu đơn giản hóa API và cải thiện khả năng trợ năng cho định tuyến. Nó nhanh chóng được cộng đồng đón nhận nhờ triết lý thiết kế khác biệt.

API của Reach Router tập trung vào:

  • <Router>: Tương tự như <BrowserRouter>.
  • Component Route (không có tên cụ thể như <Route>, bạn định nghĩa route trực tiếp bằng Component và thuộc tính path): Các Component được đặt làm con của <Router> và có thuộc tính path.
  • <Link>: Tương tự nhưng có cải tiến về relative links và focus management.
  • Hook: useMatch, useParams, useLocation, useNavigate.

Điểm khác biệt lớn nhất của Reach Router là cách nó xử lý khớp đường dẫn và xếp hạng (ranking). Khi có nhiều route có thể khớp một đường dẫn, Reach Router sẽ tự động chọn route “cụ thể nhất” (most specific) mà không cần exact hay <Switch>. Ví dụ, với đường dẫn /users/123, nếu bạn có route /users/:userId và route /users, Reach Router sẽ ưu tiên khớp /users/:userId.

Một điểm mạnh khác là cách nó xử lý định tuyến lồng nhauliên kết tương đối (relative links). Thay vì phải lặp lại cấu trúc route, bạn chỉ cần định nghĩa các route con trực tiếp bên trong Component route cha.

import { Router, Link, useMatch } from '@reach/router';

function App() {
  return (
    <Router>
      <Home path="/" />
      <About path="/about" />
      <Users path="/users/*" /> {/* Dùng "*" để cho phép route con */}
      <NotFound default />
    </Router&gt>
  );
}

function Users({ children }) {
  const match = useMatch("/users/*"); // Kiểm tra xem đường dẫn có bắt đầu bằng "/users/" không
  const userIdMatch = useMatch("/users/:userId"); // Kiểm tra khớp cụ thể

  return (
    <div>
      <h2>Trang Người Dùng</h2>
      {/* Liên kết tương đối - tự động nối vào đường dẫn cha ("/users") */}
      <nav>
        <Link to=".">Danh sách</Link> {/* /users */}
        <Link to="123">Người dùng 123</Link> {/* /users/123 */}
      </nav>

      {/* children render route con khớp */}
      {children}

      {/* Ví dụ render nội dung dựa trên match */}
      {!match && <p>Chọn người dùng để xem chi tiết.</p>}
      {userIdMatch && <p>Đang xem chi tiết người dùng.</p>}
    </div>
  );
}

function UserList() {
  return <h3>Danh sách tất cả người dùng</h3>;
}

function UserProfile({ userId }) {
  return <h3>Chi tiết người dùng: {userId}</h3>;
}

// Các Component route con được định nghĩa trực tiếp
Users.UserList = UserList; // Định nghĩa Component con
Users.UserProfile = UserProfile;

function App() {
  return (
    <Router>
      <Home path="/" />
      <About path="/about" />
      {/* Định nghĩa route con lồng trong route cha */}
      <Users path="/users/*">
        <Users.UserList path="/" /> {/* Khớp /users */}
        <Users.UserProfile path=":userId" /> {/* Khớp /users/:userId */}
      </Users>
      <NotFound default />
    </Router>
  );
}

Cú pháp này làm cho code định tuyến lồng nhau gọn gàng và dễ đọc hơn đáng kể. Thêm vào đó, Reach Router rất chú trọng đến trợ năng, đặc biệt là việc quản lý focus khi chuyển trang, một điều thường bị bỏ qua trong các thư viện khác.

Sự Khác Biệt Chính: Mổ Xẻ

Để làm rõ hơn, hãy cùng xem xét các khác biệt chính giữa React Router (trước v6) và Reach Router:

1. Cách Khớp Đường Dẫn (Path Matching)

  • React Router (pre-v6): Khớp theo tiền tố (prefix matching). Cần dùng exact để khớp chính xác toàn bộ đường dẫn. Thứ tự của <Route> bên trong <Switch> quan trọng vì nó sẽ render Component của route đầu tiên khớp.
  • Reach Router: Tự động xếp hạng và chọn route cụ thể nhất (most specific). Không cần exact hay <Switch> để giải quyết xung đột khớp tiền tố. Thứ tự các Component route trong <Router> ít quan trọng hơn vì việc xếp hạng tự động xử lý nhiều trường hợp.

2. Định Tuyến Lồng Nhau (Nested Routing)

  • React Router (pre-v6): Thường yêu cầu định nghĩa các <Route> con bên trong Component được render bởi route cha. Đường dẫn con phải lặp lại tiền tố của đường dẫn cha (hoặc dùng các kỹ thuật phức tạp hơn).
  • Reach Router: Hỗ trợ định tuyến lồng nhau trực tiếp bằng cách đặt các Component route con bên trong Component route cha và sử dụng ký tự * ở cuối đường dẫn cha. Liên kết tương đối (relative links) hoạt động tốt trong ngữ cảnh này.

3. Liên Kết (Links) và Điều Hướng

  • React Router (pre-v6): <Link> mặc định tạo liên kết tuyệt đối. Liên kết tương đối hoạt động nhưng đôi khi cần cấu hình thêm.
  • Reach Router: <Link> hỗ trợ liên kết tương đối một cách tự nhiên, giúp xây dựng cấu trúc điều hướng lồng nhau dễ dàng hơn. Ví dụ, <Link to="details"> bên trong route /users/:userId sẽ dẫn đến /users/:userId/details.

4. Trợ Năng (Accessibility)

  • React Router (pre-v6): Không có sẵn tính năng quản lý focus sau khi chuyển trang. Điều này có thể gây khó khăn cho người dùng sử dụng bàn phím hoặc trình đọc màn hình.
  • Reach Router: Được thiết kế với trợ năng làm ưu tiên. Tự động quản lý focus trên trang mới sau khi điều hướng, giúp người dùng có trải nghiệm tốt hơn.

5. API Thiết Kế

  • React Router (pre-v6): API dựa trên việc render Component (component-based) và các cấu trúc điều khiển như <Switch>.
  • Reach Router: API đơn giản và tập trung hơn vào việc định nghĩa các route trực tiếp.

Bảng dưới đây tóm tắt các khác biệt chính:

Tính năng React Router (pre-v6) Reach Router React Router v6
Cách Khớp Đường dẫn Tiền tố, cần exact cho khớp chính xác. Thứ tự <Route> trong <Switch> quan trọng. Tự động xếp hạng (cụ thể nhất). Không cần exact, <Switch>. Thứ tự ít quan trọng. Tự động xếp hạng (cụ thể nhất). Không cần exact, không dùng <Switch> (thay bằng lồng <Route>).
Định Tuyến Lồng Nhau Yêu cầu định nghĩa <Route> con bên trong Component được render. Hỗ trợ lồng <Component path="..."><ChildComponent path="..."/></Component> với *. Hỗ trợ lồng <Route path="..."><Route path="..."/></Route>. Cú pháp rất giống Reach Router.
Liên Kết Tương đối Mặc định tuyệt đối, tương đối cần cấu hình/cẩn thận hơn. Hỗ trợ liên kết tương đối tự nhiên. Hỗ trợ liên kết tương đối tự nhiên (kế thừa từ Reach Router).
Trợ Năng (Focus) Không tự động quản lý focus sau chuyển trang. Tự động quản lý focus sau chuyển trang. Tự động quản lý focus sau chuyển trang (kế thừa từ Reach Router).
Component Định Nghĩa Route <Route> riêng biệt. Component được đặt làm con của <Router> với thuộc tính path. <Route> riêng biệt, nhưng cấu trúc lồng nhau thay thế <Switch>.
Trạng thái Hiện tại Không còn là phiên bản mới nhất, ít được dùng cho dự án mới. Không còn được bảo trì độc lập, đã sáp nhập vào React Router v6. Phiên bản hiện tại, được khuyến khích sử dụng.

Reach Router’s Legacy: Ảnh Hưởng Đến React Router v6

Nhận thấy những ưu điểm vượt trội của Reach Router trong việc xử lý khớp đường dẫn, định tuyến lồng nhau và trợ năng, nhóm phát triển React Router đã đưa ra quyết định quan trọng: sáp nhập Reach Router vào React Router. Kết quả của sự sáp nhập này chính là React Router phiên bản 6.

React Router v6 đã vay mượn rất nhiều ý tưởng và API từ Reach Router. Các tính năng quan trọng như:

  • Tự động xếp hạng đường dẫn: Không cần exact hay <Switch> nữa. React Router v6 sẽ tự động chọn route cụ thể nhất.
  • Định tuyến lồng nhau: Cú pháp lồng các <Route> vào nhau đã thay thế hoàn toàn <Switch> và giúp định nghĩa các route lồng nhau trực quan hơn rất nhiều, gần giống với cách Reach Router xử lý.
  • Liên kết tương đối: <Link> trong v6 hỗ trợ liên kết tương đối theo cách tương tự Reach Router.
  • Quản lý focus: Các cải tiến về trợ năng từ Reach Router cũng được tích hợp vào v6.

Nếu bạn đã từng làm việc với Reach Router, bạn sẽ thấy API của React Router v6 quen thuộc một cách đáng kinh ngạc. Nếu bạn chỉ mới bắt đầu học React Router từ v6, việc hiểu về Reach Router giúp bạn biết được nguồn gốc của nhiều tính năng trong phiên bản mới.

Bạn có thể tham khảo bài viết chi tiết về React Router 6: Hướng Dẫn Toàn Diện trong series này để nắm vững cách sử dụng phiên bản hiện tại.

Nên Sử Dụng Thư Viện Nào Hiện Nay?

Với sự ra đời và phổ biến của React Router v6, câu trả lời là rất rõ ràng: Hãy sử dụng React Router v6.

Lý do rất đơn giản:

  • React Router v6 là phiên bản mới nhất, được bảo trì tích cực và liên tục phát triển.
  • Nó đã tích hợp những điểm mạnh nhất từ Reach Router, cung cấp API đơn giản, trực quan hơn và cải thiện trợ năng so với các phiên bản React Router cũ.
  • Cộng đồng hỗ trợ lớn mạnh, tài liệu đầy đủ.
  • Reach Router không còn được bảo trì độc lập nữa.

Việc tìm hiểu về Reach Router không phải là vô ích. Nó giúp chúng ta hiểu về sự tiến hóa của các thư viện định tuyến trong React và tại sao React Router v6 lại có thiết kế như vậy. Nó cung cấp bối cảnh lịch sử và làm sáng tỏ triết lý thiết kế đằng sau các tính năng mà bạn sẽ sử dụng hàng ngày.

Kết Luận

Định tuyến là một phần quan trọng trong việc xây dựng các ứng dụng SPA với React. Mặc dù React Router từ lâu đã là lựa chọn mặc định, sự xuất hiện của Reach Router đã mang đến những cải tiến đáng kể, đặc biệt là trong việc xử lý định tuyến lồng nhau, liên kết tương đối và trợ năng.

Cuộc “đối đầu” giữa hai thư viện này đã kết thúc với việc Reach Router sáp nhập vào React Router, khai sinh ra phiên bản 6 mạnh mẽ và thân thiện với nhà phát triển hơn. React Router v6 là sự kế thừa tốt nhất từ cả hai thế giới, mang lại trải nghiệm định tuyến tuyệt vời cho các ứng dụng React hiện đại.

Việc hiểu về những khác biệt giữa Reach Router và các phiên bản cũ của React Router không chỉ là tìm hiểu về lịch sử, mà còn giúp bạn đánh giá cao hơn những cải tiến trong React Router v6 và sử dụng nó hiệu quả hơn.

Hy vọng bài viết này đã giúp các bạn, đặc biệt là các bạn mới bắt đầu, có cái nhìn rõ ràng hơn về React Router và Reach Router, cũng như tầm quan trọng của sự sáp nhập này. Giờ đây, khi bạn làm việc với React Router v6, bạn sẽ biết rằng nhiều tính năng “hay ho” của nó có nguồn gốc từ Reach Router!

Tiếp theo trong lộ trình học React, chúng ta sẽ đi sâu vào các chủ đề quan trọng khác để hoàn thiện kỹ năng xây dựng ứng dụng của bạn. Hãy cùng nhau tiếp tục hành trình chinh phục React nhé!

Chỉ mục