+58

React router dom V6

React router dom V6

React router dom V6 xuất hiện sử dụng các tính năng tốt nhất từ các phiên bản trước đã kết thúc một thập kỉ định tuyến phía client, nó tương thích với react từ 16.8 trở lên. Bài viết này mình sẽ tổng hợp những kiến thức cơ bản đến nâng cao về react-router-dom, các ví dụ được viết và chỉnh sửa trên codesandbox để dễ theo dõi, yêu cầu cần có kiến thức cơ bản về html css javascript ReactJS và một chút thời gian.

1. Cài đặt

# Tạo ứng dụng ReactJS
npx create-react-app react-router-tutorial

# Cài thư viện
cd react-router-tutorial
npm install react-router-dom@6

Thêm bootstrap V5 vào file public/index.html để xây dựng giao diện cho nhanh

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
/>

2. Giao diện và cấu trúc component

  • Giao diện

image.png

  • Cấu trúc phân chia component
- App
  |-- Sidebar
  |-- Dashboard
import Sidebar from './components/Sidebar'
import Dashboard from './components/Dashboard'

export default function App() {
  return (
    <>
      <Sidebar />
      <Dashboard />
    </>
  )
}

3. Configuring Routes

Đầu tiên muốn kết nối ứng dụng của bạn với URL của trình duyệt thì phải import BrowserRouter và bọc nó bên ngoài toàn bộ ứng dụng chính là component App

import ReactDOM from 'react-dom'
import App from './App'
import { BrowserRouter } from 'react-router-dom'

const rootElement = document.getElementById('root')
ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  rootElement
)

Tại component Sidebar ở giao diện ban đầu chúng ta mới chỉ sử dụng thẻ a, giờ để điều hướng ta sử dụng Link

import { Link } from 'react-router-dom'

export const Sidebar = () => {
  return (
    <ul>
      <li>
        {/* <a href='/' className='nav-link'>Dashboard</a> */}
        <Link to='/' className='nav-link'>Dashboard</Link>
      </li>
      <li>
        {/* <a href='/orders' className='nav-link'>Orders</a> */}
        <Link to='/orders' className='nav-link'>Orders</Link>
      </li>
      <li>
        {/* <a href='/products' className='nav-link'>Products</a> */}
        <Link to='/products' className='nav-link'>Products</Link>
      </li>
      <li>
        {/* <a href='/customers' className='nav-link'>Customers</a> */}
        <Link to='/customers' className='nav-link'>Customers</Link>
      </li>
    </ul>
  )
}

Như vậy tại Sidebar chúng ta đã điều hướng sang các URL khác nhau, giờ với các URL đó sẽ load các component tương ứng sử dụng Routes và Route, tại component App sửa lại

import { Routes, Route } from 'react-router-dom'

export default function App() {
  return (
    <Routes>
      <Route path='/' element={<Dashboard />} />
      <Route path='/orders' element={<Orders />} />
      <Route path='/products' element={<Products />} />
      <Route path='/customers' element={<Customers />} />
    </Routes>
  )
}

Code demo: https://codesandbox.io/p/devbox/configuring-routes-c87nm?file=%2Fsrc%2FApp.jsx

4. Active Link

Ở phần trước tại component Sidebar khi chúng ta sử dụng <Link> thì output thực tế ra HTML như này

<Link className='nav-link' to='/orders'>Orders</Link>

// output ra html
<a class="nav-link" href="/orders">Orders</a>

Vẫn khá là ok nhưng giờ chúng ta muốn có thêm class active hoặc style gì đó đặc biệt cho link đang xem thì sẽ không dùng cách này được.

4-1. Active link với tên class là active

Nếu class tên là active thì easy rồi chúng ta chỉ việc thay Link thành NavLink, các bạn sẽ thấy việc output ra html cũng đã khác nhau rồi.

<NavLink className='nav-link' to='/orders'>Orders</NavLink>

// output ra html
<a aria-current="page" class="nav-link active" href="/orders">Orders</a>

Code demo: https://codesandbox.io/p/devbox/hardcore-babycat-ubh4h?file=%2Fsrc%2Fcomponents%2FSidebar.jsx

4-2. Active link với tên class khác

Nếu chúng ta muốn tên class khi được active khác đi như activated hay current-page thì viết kiểu function như này

import { NavLink } from 'react-router-dom'

const Sidebar = () => {
  const navLinkClass = ({ isActive }) => {
    return isActive ? 'nav-link activated' : 'nav-link'
  }
  return (
    <ul>
      <li>
        <NavLink to='/' className={navLinkClass}>Dashboard</NavLink>
      </li>
    </ul>
  )
}

Code demo: https://codesandbox.io/p/devbox/active-link-class-activated-crjtu?file=%2Fsrc%2Fcomponents%2FSidebar.jsx

4-3. Active link với style

Nếu không muốn trạng thái active viết theo kiểu class mà viết kiểu style thì làm như này

import { NavLink } from 'react-router-dom'

const Sidebar = () => {
  const navLinkStyle = ({ isActive }) => ({
    color: isActive ? '#fff' : '',
    backgroundColor: isActive ? '#0d6efd' : ''
  })

  return (
    <ul>
      <li>
        <NavLink to='/' className='nav-link' style={navLinkStyle}>
          Dashboard
        </NavLink>
      </li>
    </ul>
  )
}

Code demo: https://codesandbox.io/p/devbox/active-link-style-xbyei?file=%2Fsrc%2Fcomponents%2FSidebar.jsx

4-4. Custom active link

Nhiều khi đời không như mơ, vì vài lí do nào đó mà chúng ta không thể đặt class active hay style active vào chính thẻ a mà phải đặt ở thẻ cha của nó như thẻ li chẳng hạn

<li>
  <a href="/" class="active">Dashboard</a>
</li>

// phải đặt class active ở đây cơ
<li class="active">
  <a href="/">Dashboard</a>
</li>

Ví dụ sau trình bày cách tạo một Custom Active Link sử dụng useMatchuseResolvedPath trong react router

import { Link, useMatch, useResolvedPath } from 'react-router-dom'

const Sidebar = () => {
  const CustomLink = ({ children, to, ...props }) => {
    const resolved = useResolvedPath(to)
    const match = useMatch({ path: resolved.pathname, end: true })
    return (
      <li className={match ? 'active' : ''}>
        <Link to={to} {...props}>
          {children}
        </Link>
      </li>
    )
  }

  return (
    <ul>
      <CustomLink to='/'>Dashboard</CustomLink>
      <CustomLink to='/orders'>Orders</CustomLink>
      <CustomLink to='/products'>Products</CustomLink>
      <CustomLink to='/customers'>Customers</CustomLink>
    </ul>
  )
}

Code demo: https://codesandbox.io/p/devbox/proud-sound-83m1q?file=%2Fsrc%2Fcomponents%2FSidebar.jsx

5. Navigate programmatically

Ở các phần ví dụ trên khi người dùng click vào các link ở Sidebar thì chuyển trang được rồi nhưng nếu chúng ta muốn chuyển trang tự động thì sao? Ví dụ chúng ta có một form đăng nhập khi login thành công thì điều hướng họ đến một trang chủ chẳng hạn, để làm được yêu cầu này chúng ta sử dụng đến hook useNavigate

5-1. useNavigate với 1 tham số

Ví dụ bên dưới khi click vào button thì sẽ chuyển đến trang orders

import { useNavigate } from 'react-router-dom'

const Dashboard = () => {
  const navigate = useNavigate()
  
  const handleSubmit = e => {
    e.preventDefault()

    // if login success, redirect to Orders Page
    navigate('orders')
  }

  return (
    <button onClick={handleSubmit}>Submit</button>
  )
}

Code demo: https://codesandbox.io/p/devbox/use-navigate-1-f512r?file=%2Fsrc%2Fcomponents%2FDashboard.jsx

5-2. useNavigate với history

Trường hợp bạn muốn sử dụng go, goBack, goForward trong lịch sử trình duyệt.

import { useNavigate } from 'react-router-dom'

export default function App() {
  const navigate = useNavigate()

  return (
    <>
      <button onClick={() => navigate(-2)}>Go 2 pages back</button>
      <button onClick={() => navigate(-1)}>Go back</button>
      <button onClick={() => navigate(1)}>Go forward</button>
      <button onClick={() => navigate(2)}>Go 2 pages forward</button>
    </>
  )
}

Code demo: https://codesandbox.io/p/devbox/use-navigate-2-885g1?file=%2Fsrc%2Fcomponents%2FOrders.jsx

5-3. useNavigate với replace true

Sử dụng tham số thứ hai của navigate để chỉ thay đổi URL chứ không muốn URL đó lưu lại trong lịch sử trình duyệt. Kiểu như tại trang A đi tới trang B, tại trang B chúng ta click back trên trình duyệt thì sẽ không quay trở lại trang A nữa.

import { useNavigate } from 'react-router-dom'

const Dashboard = () => {
  const navigate = useNavigate()
  return (
    <button onClick={() => navigate('orders', { replace: true })}>
      Go to Orders
    </button>
  )
}

Code demo: https://codesandbox.io/p/devbox/modest-dan-sb4oi?file=%2Fsrc%2Fcomponents%2FDashboard.jsx

5-4. useNavigate với passing data

  • Tình huống đặt ra là tại componentA khi navigate chuyển sang componentB sẽ kèm thêm một id là 6996
  • Tại componentB sẽ nhận được data thông qua useLocation
import { useNavigate } from 'react-router-dom'

const componentA = () => {
  const navigate = useNavigate()
  
  return (
    <button onClick={() => navigate("orders", { state: { id: "6996" } })}>
      Send ID to componentB
    </button>
  )
}
import { useLocation } from 'react-router-dom'

const ComponentB = () => {
  const location = useLocation()
  return <h1>ID nhận từ componentA: {location.state?.id}</h1>
}

Code demo: https://codesandbox.io/p/devbox/use-navigate-4-5vvsgv?file=%2Fsrc%2Fcomponents%2FComponentA.jsx

5-5. Component Navigate

Sử dụng component Navigate để chuyển trang

import { Navigate } from 'react-router-dom'
import Dashboard from './Dashboard'

const PrivateRoutes = () => {
  const isAuth = true

  return isAuth ? <Dashboard /> : <Navigate to='/login' />
}

6. Not Found Routes - 404

Trường hợp này xảy ra nếu người dùng nhập vào một URL không hợp lệ như https://example.com/abcdef thì chúng ta sẽ điều hướng tới trang 404. Chúng ta để Not Found Routes này ở dưới cùng để nếu các route ở trên không cái nào trùng khớp thì sẽ đến route này.

<Routes>
  <Route path='/' element={<Dashboard />} />
  <Route path='*' element={<NotFound />} />
</Routes>

Code demo: https://codesandbox.io/p/devbox/not-found-routes-404-y5gjs?file=%2Fsrc%2FApp.jsx

7. Nested Routes

Nested Routes để lồng component con vào trong component cha.

<Routes>
  <Route path='/' element={<Dashboard />} />
  <Route path='/products' element={<Products />}>
    <Route path='laptop' element={<Laptop />} />
    <Route path='desktop' element={<Desktop />} />
  </Route>
</Routes>
import { Outlet } from 'react-router-dom'

const Products = () => (
  <>
    <h1>Products</h1>
    <Outlet />
  </>
)

Như code ở trên

  • Nếu truy cập vào localhost:3000 sẽ load component Dashboard
  • Nếu truy cập vào localhost:3000/products sẽ load component Products
  • Nếu truy cập vào localhost:3000/products/laptop sẽ load Products bên trong có component Laptop
  • Nếu truy cập vào localhost:3000/products/desktop sẽ load Products bên trong có component Desktop
  • Lưu ý tại component cha là Products ta sử dụng Outlet để xác định vị trí hiển thị component con khi route trùng khớp
  • Code demo: https://codesandbox.io/p/devbox/nested-routes-ub8ee?file=%2Fsrc%2FApp.jsx

8. Index routes

  • Như ví dụ ở trên thì khi truy cập vào localhost:3000/products sẽ load component Products và không hiển thị conponent con nào cả
  • Điều chúng ta mong muốn là vẫn URL như vậy nhưng hiển thị một conponent con nào đó ở ngay component cha
  • Để làm được điều này ta sử dụng index và truyền conponent con muốn được hiển thị
  • Ví dụ bên dưới: nếu truy cập vào localhost:3000/products sẽ load component Products bên trong có component BestSeller
<Routes>
  <Route path='/products' element={<Products />}>
    <Route index element={<BestSeller />} />
    <Route path='laptop' element={<Laptop />} />
    <Route path='desktop' element={<Desktop />} />
  </Route>
</Routes>

9. Dynamic routes

<Routes>
  <Route path='/' element={<Dashboard />} />
  <Route path='/courses' element={<Courses />} />
  <Route path='/courses/:courseId' element={<CourseDetail />} />
</Routes>
import { useParams } from 'react-router-dom'

const CustomerDetail = () => {
  const params = useParams()
  return <h2>Chi tiết khóa học: {params.courseId}</h2>
}
  • Dynamic routes giúp chúng ta giải quyết các routes động, routes thay đổi theo một cấu trúc đã được định nghĩa sẵn.
  • Ví dụ ta có URL theo kiểu courses/html, courses/css, courses/javascript thì có thể mô hình hóa nó thành courses/:courseId
  • Tại CourseDetail ta sử dụng hook useParams để lấy tham số trên URL.
  • Nếu ta định nghĩa route là courses/:courseId thì lúc lấy giá trị cũng lấy đúng tên là params.courseId
  • Code demo: https://codesandbox.io/p/devbox/dynamic-routes-1iedr?file=%2Fsrc%2FApp.jsx

9-1. Dynamic routes với các route cố định

<Routes>
  <Route path='/courses' element={<Courses />} />
  <Route path='/courses/:courseId' element={<CourseDetail />} />
  <Route path='/courses/add-course' element={<AddCourse />} />
  <Route path='/courses/edit-course' element={<EditCourse />} />
</Routes>
  • Khi sử dụng Dynamic routes ta có URL theo kiểu courses/html, courses/css, courses/javascript thì đã mô hình hóa nó thành courses/:courseId
  • Trong một vài trường hợp route cố định như courses/add-course hay courses/edit-course thì ta sẽ khai báo route để bắt các trường hợp này
  • Code demo: https://codesandbox.io/p/devbox/dynamic-routes-2-zb3h2?file=%2Fsrc%2FApp.jsx

9-2. Multiple dynamic routes

<Routes>
  <Route path='/courses' element={<Courses />} />
  <Route path='/courses/:courseType/' element={<CourseType />} />
  <Route path='/courses/:courseType/:courseId' element={<CourseDetail />} />
</Routes>

10. Search params

import { useSearchParams } from 'react-router-dom'

let [searchParams, setSearchParams] = useSearchParams()

Hook useSearchParams được sử dụng để đọc và sửa đổi query string trên URL, hook này trả về một mảng gồm hai giá trị: tham số tìm kiếm và một hàm để thay đổi.

Để thay đổi searchParams

<button onClick={() => setSearchParams({product: 'laptop'})}>
  Laptop
</button>
<button onClick={() => setSearchParams({product: 'laptop', stock: 'in-stock'})}>
  Còn hàng
</button>
<button onClick={() => setSearchParams({})}>
  Clear
</button>
  • Khi click vào button Laptop => https://example.com/?product=laptop
  • Khi click vào button Còn hàng => https://example.com/?product=laptop&stock=in-stock
  • Khi click vào button Clear => https://example.com

Để đọc searchParams

const productType = searchParams.get('product'); // laptop
const stock = searchParams.get('stock'); // in-stock

Code demo: https://codesandbox.io/p/devbox/search-params-9s77sh?file=%2Fsrc%2Fcomponents%2FDashboard.jsx

11. Protected routes

Giả sử ứng dụng của chúng ta có hai phần: publicprivate

  • Phần public thì ai cũng có thể truy cập được như trang chủ, tin tức...
  • Phần private thì phải đăng nhập vào mới xem được như trang cá nhân, trang account...

Về hành vi đối với người dùng

  • Nếu đăng nhập rồi thì truy cập được tất cả các link của public hay private
  • Nếu chưa thì chỉ truy cập được các trang public, nếu người dùng vẫn cố truy cập vào các trang private thì ta điều hướng họ sang trang login hay trang nào thì tùy nghiệp vụ.

Bước 1: Nhóm các route cần bảo vệ vào trong PrivateRoutes

<Routes>
  <Route path='/' element={<Home />} />
  <Route path='/news' element={<News />} />
  <Route element={<PrivateRoutes />}>
    <Route path='/personal' element={<Personal />} />
    <Route path='/account' element={<Account />} />
  </Route>
</Routes>

Bước 2: Trong PrivateRoutes sẽ kiểm tra login hay chưa để xử lý

import { Navigate, Outlet } from 'react-router-dom'

const PrivateRoutes = () => {
  const isAuth = true

  return isAuth ? <Outlet /> : <Navigate to='/login' />
}

Code demo: https://codesandbox.io/p/devbox/protected-routes-wnd2p2?file=%2Fsrc%2FApp.jsx

12. Lazy loading

Lazy Loading là một kỹ thuật để tối ưu hoá ứng dụng, khi chuyển trang ứng dụng sẽ chỉ load những component cần thiết, điều này giúp trang web chuyển động mượt mà, nhanh chóng, tăng trải nghiệm người dùng.

Code bên dưới khi CHƯA áp dụng Lazy Loading cho component About

import { Routes, Route } from 'react-router-dom'
import Dashboard from './components/Dashboard'
import About from './components/About'

export default function App() {
  return (
    <Routes>
      <Route path='/' element={<Dashboard />} />
      <Route path='/about' element={<About />} />
    </Routes>
  )
}

Còn bên dưới khi đã áp dụng Lazy Loading cho component About

import React from 'react'
import { Routes, Route } from 'react-router-dom'
import Dashboard from './components/Dashboard'
const LazyAbout = React.lazy(() => import('./components/About'))

export default function App() {
  return (
    <Routes>
      <Route path='/' element={<Dashboard />} />
      <Route
        path='/about'
        element={
          <React.Suspense fallback={<div>Loading...</div>}>
            <LazyAbout />
          </React.Suspense>
        }
      />
    </Routes>
  )
}

Code demo: https://codesandbox.io/p/devbox/lazy-loading-qdi5n?file=%2Fsrc%2FApp.jsx

Để áp dụng kĩ thuật Lazy Loading chúng ta cần:

  • Dynamic import trong React
  • React.lazy()
  • React.Suspense()
  • Vì ứng dụng sử dụng create-react-app nên đã cài đặt và cấu hình sẵn webpack
  • Khi ta dùng import About from './components/About' thì webpack đã đóng gói nó cả trong một file js ban đầu nên khá nặng.
  • Khi người dùng lần đầu tiên vào ứng dụng thì sẽ phải tải và thực thi code bên trong nên sẽ mất thời gian.
  • Việc sử dụng Lazy Load thì ta đã tách riêng file js của trang About theo kiểu code splitting ra khỏi file js ban đầu.
  • Khi người dùng vào trang /about thì hiện loading và bắt đầu tải, thực thi file js riêng này.

13. Route Objects

Hook useRoutes cho phép bạn xác định các tuyến đường dưới dạng javascript object thuần thay cho <Routes><Route>. Đây chỉ là một phong cách tùy chọn cho những người không muốn sử dụng JSX

Code bên dưới áp dụng <Routes><Route>

import { Routes, Route } from 'react-router-dom'

export default function App() {
  return (
    <Routes>
      <Route path='/' element={<Dashboard />} />
      <Route path='/products' element={<Products />}>
        <Route index element={<BestSeller />} />
        <Route path='laptop' element={<Laptop />} />
        <Route path='desktop' element={<Desktop />} />
      </Route>
      <Route path='*' element={<NoMatch />} />
    </Routes>
  )
}

Code bên dưới áp dụng hook useRoutes

import { useRoutes } from 'react-router-dom'

const routes = [
  { path: '/', element: <Dashboard /> },
  {
    path: '/products',
    element: <Products />,
    children: [
      { index: true, element: <BestSeller /> },
      { path: 'laptop', element: <Laptop /> },
      { path: 'desktop', element: <Desktop /> }
    ]
  },
  { path: '*', element: <NoMatch /> }
]

export default function App() {
  const element = useRoutes(routes)

  return element
}

Code demo: https://codesandbox.io/p/devbox/route-objects-dvfdwx?file=%2Fsrc%2FApp.jsx

14. Scroll to top

Trong thực tế chúng ta hay gặp phải trường hợp đang ở trang A và cuộn đến cuối trang này, khi chuyển sang trang B thì thanh cuộn vẫn ở dưới gây chút khó chịu. Demo tại đây: https://codesandbox.io/p/devbox/async-feather-mg2o39?file=%2Fsrc%2Fcomponents%2FPart1.jsx

Để fix vấn đề này chúng ta có thể tạo 1 component scrollToTop và thêm nó vào trong đoạn config route

<div>
  <ScrollToTop />
  <Routes>
    <Route path='/' element={<Dashboard />} />
    <Route path='/orders' element={<Orders />} />
    <Route path='/products' element={<Products />} />
  </Routes>
</div>
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'

const ScrollToTop = () => {
  const { pathname } = useLocation()

  useEffect(() => {
    window && window.scrollTo(0, 0)
  }, [pathname])

  return null
}

export default ScrollToTop

Demo đã fix lỗi tại đây: https://codesandbox.io/p/devbox/scroll-to-top-ngon-vwhklx?file=%2Fsrc%2Fcomponents%2FPart1.jsx

15. Can't setState on unmounted component

  • Khi chúng ta đang ở trang Home và lấy dữ liệu từ API
  • Dữ liệu chưa GET xong thì người dùng chuyển sang trang About
  • Component Home lúc đó đã bị unmount
  • Sau đó dữ liệu từ API được trả về và setState()
  • Component Home không còn để update

can't setState on unmounted component

async function handleSubmit() {
  setPending(true)
  await post('/someapi') // component might unmount while we're waiting
  setPending(false)
}
let isMountedRef = useRef(false)
useEffect(() => {
  isMountedRef.current = true
  return () => {
    isMountedRef.current = false
  }
}, [])

async function handleSubmit() {
  setPending(true)
  await post('/someapi')
  if (isMountedRef.current) {
    setPending(false)
  }
}

Bài viết đến đây là hết, hi vọng với bài viết này các bạn đã thêm được nhiều kiến thức bổ ích. Hẹn gặp lại các bạn ở bài viết tiếp theo!

  • Liên hệ: trungnt256
  • Hướng dẫn sử dụng Font Awesome 5 bản miễn phí tại đây
  • Hướng dẫn tìm hiểu SCSS cơ bản tại đây

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí