|
| 1 | +## First start |
| 2 | + |
| 3 | +In this repository I will discuss about |
| 4 | + |
| 5 | +- How to define the basic router information in this project ? |
| 6 | +- How to use lazy loading in react-router and customize it ? |
| 7 | +- How to use Suspense to create loading page for loading process ? |
| 8 | +- How to validate a route ? |
| 9 | +- How to protect a route ? |
| 10 | + |
| 11 | +For more information about this project. |
| 12 | + |
| 13 | +1. You can read detail about advanced structure of Webpack + React + TS project in this [repository](https://github.com/anhchangvt1994/webpack-project--template-react-ts). |
| 14 | +2. You can read about react-router in [here](https://reactrouter.com/en/main). |
| 15 | + |
| 16 | +## Table of contents |
| 17 | + |
| 18 | +1. [Install](#install) |
| 19 | +2. [Introduction](#introduction) |
| 20 | + |
| 21 | +<h2>Install</h2> |
| 22 | + |
| 23 | +##### Expect Node 18.x or higher |
| 24 | + |
| 25 | +Clone source with SSH url: |
| 26 | + |
| 27 | +```bash |
| 28 | +git clone https://github.com/anhchangvt1994/webpack-project--template-react-ts__react-router |
| 29 | +``` |
| 30 | + |
| 31 | +Install: |
| 32 | + |
| 33 | +```bash |
| 34 | +cd webpack-project--template-react-ts__react-router |
| 35 | +``` |
| 36 | + |
| 37 | +If use npm |
| 38 | + |
| 39 | +```bash |
| 40 | +npm install |
| 41 | +``` |
| 42 | + |
| 43 | +If use yarn 1.x |
| 44 | + |
| 45 | +```bash |
| 46 | +yarn install |
| 47 | +``` |
| 48 | + |
| 49 | +<h2>Introduction</h2> |
| 50 | + |
| 51 | +### Table of benefit information that you must know |
| 52 | + |
| 53 | +- [Define router information](#define) |
| 54 | +- [lazy-loading](#lazy-loading) |
| 55 | +- [Suspense](#Suspense) |
| 56 | +- [Validate on route](#validate) |
| 57 | +- [Protect on route](#protect) |
| 58 | + |
| 59 | +<h3 id="define">Define router information</h3> |
| 60 | +In this project, you can define router information in two ways |
| 61 | + |
| 62 | +1. Immediacy |
| 63 | + |
| 64 | +This way will fast and easy to define and create a react-router. See code below. |
| 65 | + |
| 66 | +```jsx |
| 67 | +// config/router/index.jsx |
| 68 | +import Layout from 'Layout.jsx' |
| 69 | +import HomePage from 'pages/HomePage.jsx' |
| 70 | + |
| 71 | +const routes = [ |
| 72 | + { |
| 73 | + path: '/', |
| 74 | + element: <Layout />, |
| 75 | + children: [ |
| 76 | + { |
| 77 | + index: true, |
| 78 | + path: '/', |
| 79 | + element: <HomePage />, |
| 80 | + }, |
| 81 | + ... |
| 82 | + ], |
| 83 | + }, |
| 84 | +] |
| 85 | + |
| 86 | +const router = createBrowserRouter(routes, { |
| 87 | + basename: '/', |
| 88 | +}) |
| 89 | +``` |
| 90 | + |
| 91 | +2. Define and use it by using environment variables |
| 92 | + |
| 93 | +This way will make you spend more time to define and create react-router-dom. But other way you can reuse it to compare route's name, makesure right result when create route's path by using `<Link to={path} />` and more. See code below |
| 94 | + |
| 95 | +```javascript |
| 96 | +// env.router.mjs |
| 97 | +export default { |
| 98 | + prefix: 'router', |
| 99 | + data: { |
| 100 | + base: { |
| 101 | + path: '/', |
| 102 | + }, |
| 103 | + home: { |
| 104 | + id: 'HomePage', |
| 105 | + path: '/', |
| 106 | + }, |
| 107 | + home: { |
| 108 | + id: 'ContentPage', |
| 109 | + path: '/:slugs', |
| 110 | + }, |
| 111 | + ... |
| 112 | + }, |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +```jsx |
| 117 | +// config/router/index.jsx |
| 118 | +import Layout from 'Layout.jsx' |
| 119 | +import HomePage from 'pages/HomePage.jsx' |
| 120 | + |
| 121 | +const routes = [ |
| 122 | + { |
| 123 | + path: import.meta.env.ROUTER_BASE_PATH, |
| 124 | + element: <Layout />, |
| 125 | + children: [ |
| 126 | + { |
| 127 | + index: true, |
| 128 | + path: import.meta.env.ROUTER_HOME_PATH, |
| 129 | + element: <HomePage />, |
| 130 | + }, |
| 131 | + ... |
| 132 | + ], |
| 133 | + }, |
| 134 | +] |
| 135 | + |
| 136 | +const router = createBrowserRouter(routes, { |
| 137 | + basename: '/', |
| 138 | +}) |
| 139 | +``` |
| 140 | + |
| 141 | +```jsx |
| 142 | +// HomePage.jsx |
| 143 | +const pathInfo = generatePath(import.meta.env.ROUTER_CONTENT_PATH, { |
| 144 | + slugs: 'a-b-c-d-1234', |
| 145 | +}) |
| 146 | + |
| 147 | +return <Link to={pathInfo}></Link> |
| 148 | +``` |
| 149 | +
|
| 150 | +Imagine that what happend if you use the first solution to create react-router-dom information and change a path. Are you sure that you changed all of them ? |
| 151 | +
|
| 152 | +<h3>lazy-loading</h3> |
| 153 | +
|
| 154 | +In react, it already introduced a simple way to create a lazy-loading by using dynamic import with [lazy](https://reactjs.org/docs/code-splitting.html#reactlazy) method. See code below and more about [lazy-loading route](https://www.positronx.io/react-lazy-loading-router-using-react-router-dom-tutorial/) |
| 155 | +
|
| 156 | +```jsx |
| 157 | +// config/router/index.jsx |
| 158 | +import Layout from 'Layout.jsx' |
| 159 | + |
| 160 | +const HomePage = lazy(() => import('pages/HomePage.jsx')); |
| 161 | + |
| 162 | +const routes = [ |
| 163 | + { |
| 164 | + path: import.meta.env.ROUTER_BASE_PATH, |
| 165 | + element: <Layout />, |
| 166 | + children: [ |
| 167 | + { |
| 168 | + index: true, |
| 169 | + path: import.meta.env.ROUTER_HOME_PATH, |
| 170 | + element: <HomePage />, |
| 171 | + }, |
| 172 | + ... |
| 173 | + ], |
| 174 | + }, |
| 175 | +] |
| 176 | + |
| 177 | +const router = createBrowserRouter(routes, { |
| 178 | + basename: '/', |
| 179 | +}) |
| 180 | +``` |
| 181 | +
|
| 182 | +<h3>Suspense</h3> |
| 183 | +
|
| 184 | +Suspense is a loading resolver solution. Imagine that your page is loading the HomePage resource (by using import dynamic on route) or await an API requesting, and your internet connection is so bad. Bumb! You see a blank page or a current page in so long of time, and that's the time you have to show a loading page or a skeleton. |
| 185 | +
|
| 186 | +In this section, I will discuss about handling the loading page when using lazy-loading routes. |
| 187 | +
|
| 188 | +Continue the problem above, we can resolve in two ways |
| 189 | +
|
| 190 | +1. Listen on Route and Listen on Hook |
| 191 | + Step by step like this |
| 192 | +
|
| 193 | +- When a route init or change, the Layout will re-render. I will turn on load in this event. |
| 194 | +- When lazy-loading finish, the page component will active hooks. I will turn off loading screen at before logic code. |
| 195 | +
|
| 196 | +route init/change > Layout re-render > turn on loading screen > lazy-loading route finish > lifecycle hooks of page actived > turn off loading screen. |
| 197 | +
|
| 198 | +2. Use Suspense |
| 199 | +
|
| 200 | +In the first solution, we use router event and react hook + store to keep flag of on/off the loading screen. It means we have to : |
| 201 | +
|
| 202 | +- Define a isLoading flag in store. |
| 203 | +- Define the turn on script in Layout. |
| 204 | +- Define the turn of script in each page's hook. |
| 205 | +
|
| 206 | +I think that's run well, but not good for management. |
| 207 | +
|
| 208 | +In the second solution, we will use Suspense to resolve loading screen with better management. See code below. |
| 209 | +
|
| 210 | +```jsx |
| 211 | +// config/router/index.jsx |
| 212 | +import Layout from 'Layout.jsx' |
| 213 | + |
| 214 | +const HomePage = lazy(() => import('pages/HomePage.jsx')); |
| 215 | + |
| 216 | +const routes = [ |
| 217 | + { |
| 218 | + path: import.meta.env.ROUTER_BASE_PATH, |
| 219 | + element: <Layout />, |
| 220 | + children: [ |
| 221 | + { |
| 222 | + index: true, |
| 223 | + path: import.meta.env.ROUTER_HOME_PATH, |
| 224 | + element: <HomePage />, |
| 225 | + }, |
| 226 | + ... |
| 227 | + ], |
| 228 | + }, |
| 229 | +] |
| 230 | + |
| 231 | +const router = createBrowserRouter(routes, { |
| 232 | + basename: '/', |
| 233 | +}) |
| 234 | +``` |
| 235 | +
|
| 236 | +```jsx |
| 237 | +import LoadingBoundary from 'utils/LoadingBoundary' |
| 238 | +import LoadingPageComponent from 'components/LoadingPageComponent' |
| 239 | + |
| 240 | +function Layout() { |
| 241 | + const location = useLocation() |
| 242 | + return ( |
| 243 | + <div className="layout"> |
| 244 | + <LoadingBoundary |
| 245 | + key={location.pathname} |
| 246 | + delay={150} |
| 247 | + fallback={<LoadingPageComponent />} |
| 248 | + > |
| 249 | + <Outlet /> |
| 250 | + </LoadingBoundary> |
| 251 | + </div> |
| 252 | + ) |
| 253 | +} // App() |
| 254 | + |
| 255 | +export default Layout |
| 256 | +``` |
| 257 | +
|
| 258 | +Easy! And you're finish him. You created loading screen in just two files instead of multiples files like the first solution. |
| 259 | +
|
| 260 | +NOTE: The `<Suspense></Suspense>` tag is already add into `<LoadingBoundary></LoadingBoundary>` used to custom delay time to show loading screen. If you need to know more about this customization, you can search on `LoadingBoundary`. |
| 261 | +
|
| 262 | +<h3 id="validate">Validate on route</h3> |
| 263 | +
|
| 264 | +You can use regex to validate route in **react-router v5**, See code below |
| 265 | +
|
| 266 | +```javascript |
| 267 | +// /a-b-c-d-e1234 -> wrong |
| 268 | +// /a-b-c-d-1234 -> right |
| 269 | +{ |
| 270 | + path: '/:slugs([a-zA-Z]-(\\d+$))' |
| 271 | +} |
| 272 | +``` |
| 273 | +
|
| 274 | +That really cool feature! But this feature was [removed in react-router v6](https://reactrouter.com/en/main/start/faq#what-happened-to-regexp-routes-paths). So, you can't use regex to validate on route, instead that you can create a new hook for resolve the nesscessary of validation. |
| 275 | +
|
| 276 | +In this project, I already create and integrate that hook for you. See code below, and readmore about inline comment in code. |
| 277 | +
|
| 278 | +```javascript |
| 279 | +{ |
| 280 | + path: import.meta.env.ROUTER_CONTENT_PATH, |
| 281 | + element: withLazy(() => import('pages/ContentPage')), |
| 282 | + handle: { |
| 283 | + // It means validate and split method is params's method |
| 284 | + params: { |
| 285 | + // p is shorthand of params |
| 286 | + // If p.slugs is a string, we can validate it. Else, it is an undefined we can pass it. |
| 287 | + validate(p) { |
| 288 | + if (typeof p.slugs === 'string') { |
| 289 | + // /a-b-c-d-e1234 -> go to not found page |
| 290 | + // /a-b-c-d-1234 -> go to target page |
| 291 | + return /[a-zA-Z-_.]+[a-zA-Z]-(\d+$)/.test(p.slugs) |
| 292 | + } |
| 293 | + |
| 294 | + return true |
| 295 | + }, |
| 296 | + // split method is a solution using to sperarate params to small and detail params. |
| 297 | + // not same vue-router, react-router doesn't have multiple params define in each slash |
| 298 | + // vue-router: /:title-:id -> right |
| 299 | + // react-router: /:title-:id -> wrong |
| 300 | + // so, to split slugs params to detail params information, we have to custom it by handle.split. |
| 301 | + split(p) { |
| 302 | + return { |
| 303 | + slug: p.slugs?.match(/^[a-zA-Z-_.]+[a-zA-Z]/)?.[0], |
| 304 | + id: p.slugs?.match(/\d+$/)?.[0], |
| 305 | + } |
| 306 | + }, |
| 307 | + } |
| 308 | + }, |
| 309 | +``` |
| 310 | +
|
| 311 | +Tada! it's finish! |
| 312 | +
|
| 313 | +As you can see, in react you must know more than and do more than, to create helpful utils and hooks for your project. But in this project the react-router looks like more simple. |
| 314 | +
|
| 315 | +<h3 id="protect">Protect on route</h3> |
| 316 | +
|
| 317 | +You can protect route by using the **handle.protect method**. |
| 318 | +Imagine that you have a route only allow V.I.P user, then you need to prevent other user enter that V.I.P route. In this case you can use protect route to resolve it. See code below |
| 319 | +
|
| 320 | +```javascript |
| 321 | +{ |
| 322 | + path: import.meta.env.ROUTER_CONTENT_PATH, |
| 323 | + element: withLazy(() => import('pages/ContentPage')), |
| 324 | + handle: { |
| 325 | + protect() { |
| 326 | + const userInfo = useUserInfo() |
| 327 | + return userInfo.isVip |
| 328 | + } |
| 329 | + }, |
| 330 | +``` |
| 331 | +
|
| 332 | +Makesure your protect function is a **Pure Function**, it make your result will always right. |
0 commit comments