how-to-implement-routing.md (7354B)
1 ## How to Implement Routing and Navigation 2 3 Let's see how a custom routing solution under 100 lines of code may look like. 4 5 First, you will need to implement the **list of application routes** in which each route can be 6 represented as an object with properties of `path` (a parametrized URL path string), `action` 7 (a function), and optionally `children` (a list of sub-routes, each of which is a route object). 8 The `action` function returns anything - a string, a React component, etc. For example: 9 10 #### `src/routes/index.js` 11 12 ```js 13 export default [ 14 { 15 path: '/tasks', 16 action() { 17 const resp = await fetch('/api/tasks'); 18 const data = await resp.json(); 19 return data && { 20 title: `To-do (${data.length})`, 21 component: <TodoList {...data} /> 22 }; 23 } 24 }, 25 { 26 path: '/tasks/:id', 27 action({ params }) { 28 const resp = await fetch(`/api/tasks/${params.id}`); 29 const data = await resp.json(); 30 return data && { 31 title: data.title, 32 component: <TodoItem {...data} /> 33 }; 34 } 35 } 36 ]; 37 ``` 38 39 Next, implement a **URL Matcher** function that will be responsible for matching a parametrized 40 path string to the actual URL. For example, calling `matchURI('/tasks/:id', '/tasks/123')` must 41 return `{ id: '123' }` while calling `matchURI('/tasks/:id', '/foo')` must return `null`. 42 Fortunately, there is a great library called [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp) 43 that makes this task very easy. Here is how a URL matcher function may look like: 44 45 #### `src/core/router.js` 46 47 ```js 48 import toRegExp from 'path-to-regexp'; 49 50 function matchURI(path, uri) { 51 const keys = []; 52 const pattern = toRegExp(path, keys); // TODO: Use caching 53 const match = pattern.exec(uri); 54 if (!match) return null; 55 const params = Object.create(null); 56 for (let i = 1; i < match.length; i++) { 57 params[keys[i - 1].name] = 58 match[i] !== undefined ? match[i] : undefined; 59 } 60 return params; 61 } 62 ``` 63 64 Finally, implement a **Route Resolver** function that given a list of routes and a URL/context 65 should find the first route matching the provided URL string, execute its action method, and if the 66 action method returns anything other than `null` or `undefined` return that to the caller. 67 Otherwise, it should continue iterating over the remaining routes. If none of the routes match to the 68 provided URL string, it should throw an exception (Not found). Here is how this function may look like: 69 70 #### `src/core/router.js` 71 72 ```js 73 import toRegExp from 'path-to-regexp'; 74 75 function matchURI(path, uri) { ... } // See above 76 77 async function resolve(routes, context) { 78 for (const route of routes) { 79 const uri = context.error ? '/error' : context.pathname; 80 const params = matchURI(route.path, uri); 81 if (!params) continue; 82 const result = await route.action({ ...context, params }); 83 if (result) return result; 84 } 85 const error = new Error('Not found'); 86 error.status = 404; 87 throw error; 88 } 89 90 export default { resolve }; 91 ``` 92 93 That's it! Here is a usage example: 94 95 ```js 96 import router from './core/router'; 97 import routes from './routes'; 98 99 router.resolve(routes, { pathname: '/tasks' }).then(result => { 100 console.log(result); 101 // => { title: 'To-do', component: <TodoList .../> } 102 }); 103 ``` 104 105 While you can use this as it is on the server, in a browser environment it must be combined with a 106 client-side navigation solution. You can use [`history`](https://github.com/ReactTraining/history) 107 npm module to handles this task for you. It is the same library used in React Router, sort of a 108 wrapper over [HTML5 History API](https://developer.mozilla.org/docs/Web/API/History_API) that 109 handles all the tricky browser compatibility issues related to client-side navigation. 110 111 First, create `src/core/history.js` file that will initialize a new instance of the `history` module 112 and export is as a singleton: 113 114 #### `src/core/history.js` 115 116 ```js 117 import createHistory from 'history/lib/createBrowserHistory'; 118 import useQueries from 'history/lib/useQueries'; 119 export default useQueries(createHistory)(); 120 ``` 121 122 Then plug it in, in your client-side bootstrap code as follows: 123 124 #### `src/client.js` 125 126 ```js 127 import ReactDOM from 'react-dom'; 128 import history from './core/history'; 129 import router from './core/router'; 130 import routes from './routes'; 131 132 const container = document.getElementById('root'); 133 134 function renderRouteOutput({ title, component }) { 135 ReactDOM.render(component, container, () => { 136 document.title = title; 137 }); 138 } 139 140 function render(location) { 141 router.resolve(routes, location) 142 .then(renderRouteOutput) 143 .catch(error => router.resolve(routes, { ...location, error }) 144 .then(renderRouteOutput)); 145 } 146 147 render(history.getCurrentLocation()); // render the current URL 148 history.listen(render); 149 ``` 150 151 Whenever a new location is pushed into the `history` stack, the `render()` method will be called, 152 that itself calls the router's `resolve()` method and renders the returned from it React component 153 into the DOM. 154 155 In order to trigger client-side navigation without causing full-page refresh, you need to use 156 `history.push()` method, for example: 157 158 ```js 159 import React from 'react'; 160 import history from '../core/history'; 161 162 class App extends React.Component { 163 transition = event => { 164 event.preventDefault(); 165 history.push({ 166 pathname: event.currentTarget.pathname, 167 search: event.currentTarget.search 168 }); 169 }; 170 render() { 171 return ( 172 <ul> 173 <li><a href="/" onClick={this.transition}>Home</a></li> 174 <li><a href="/one" onClick={this.transition}>One</a></li> 175 <li><a href="/two" onClick={this.transition}>Two</a></li> 176 </ul> 177 ); 178 } 179 } 180 ``` 181 182 Though, it is a common practice to extract that transitioning functionality into a stand-alone 183 (`Link`) component that can be used as follows: 184 185 ```html 186 <Link to="/tasks/123">View Task #123</Link> 187 ``` 188 189 ### Routing in React Starter Kit 190 191 React Starter Kit (RSK) uses [Universal Router](https://github.com/kriasoft/universal-router) npm 192 module that is built around the same concepts demonstrated earlier with the major differences that 193 it supports nested routes and provides you with the helper `Link` React component. It can be seen as 194 a lightweight more flexible alternative to React Router. 195 196 - It has simple code with minimum dependencies (just `path-to-regexp` and `babel-runtime`) 197 - It can be used with any JavaScript framework such as React, Vue.js etc 198 - It uses the same middleware approach used in Express and Koa, making it easy to learn 199 - It uses the exact same API and implementation to be used in both Node.js and browser environments 200 201 The [Getting Started page](https://github.com/kriasoft/universal-router/blob/master/docs/getting-started.md) 202 has a few examples how to use it. 203 204 ### Related Articles 205 206 - [You might not need React Router](https://medium.freecodecamp.com/you-might-not-need-react-router-38673620f3d) by Konstantin Tarkus 207 208 ### Related Projects 209 210 - [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp) 211 - [`history`](https://github.com/ReactTraining/history) 212 - [Universal Router](https://github.com/kriasoft/universal-router) 213 214 ### Related Discussions 215 216 - [How to Implement Routing and Navigation](https://github.com/kriasoft/react-starter-kit/issues/748) 217 - [How to Add a Route to RSK?](https://github.com/kriasoft/react-starter-kit/issues/754)