ff-stream-web

git clone git://archive.git.mtrnord.blog/MTRNord/ff-stream-web.git
Log | Files | Refs | README | LICENSE

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)