|
| 1 | +# @reactions/router |
| 2 | + |
| 3 | +## Installation |
| 4 | + |
| 5 | +```bash |
| 6 | +npm install @reactions/router |
| 7 | +# or |
| 8 | +yarn add @reactions/router |
| 9 | +``` |
| 10 | + |
| 11 | +And then import it: |
| 12 | + |
| 13 | +```js |
| 14 | +// using es modules |
| 15 | +import { Router, Link } from "@reactions/router"; |
| 16 | + |
| 17 | +// common.js |
| 18 | +const { Router, Link } = require("@reactions/router"); |
| 19 | + |
| 20 | +// AMD |
| 21 | +// I've forgotten but it should work. |
| 22 | +``` |
| 23 | + |
| 24 | +Or use script tags and globals. |
| 25 | + |
| 26 | +```html |
| 27 | +<script src="https://unpkg.com/@reactions/router"></script> |
| 28 | +``` |
| 29 | + |
| 30 | +And then grab it off the global like so: |
| 31 | + |
| 32 | +```js |
| 33 | +ReactionsRouter.Router; |
| 34 | +ReactionsRouter.Link; |
| 35 | +``` |
| 36 | + |
| 37 | +## Take 5 minutes and read this first |
| 38 | + |
| 39 | +### Rendering |
| 40 | + |
| 41 | +Routers select a child to render based on the child's path. The children are just other components that could be rendered on their own. |
| 42 | + |
| 43 | +```js |
| 44 | +import { React } from "react"; |
| 45 | +import { render } from "react-dom"; |
| 46 | +import { Router, Link } from "@reactions/router"; |
| 47 | + |
| 48 | +const Home = () => <div>Home</div>; |
| 49 | +const Dash = () => <div>Dash</div>; |
| 50 | + |
| 51 | +render( |
| 52 | + <Router> |
| 53 | + <Home path="/" /> |
| 54 | + <Dash path="dashboard" /> |
| 55 | + </Router> |
| 56 | +); |
| 57 | +``` |
| 58 | + |
| 59 | +### Navigate with Link |
| 60 | + |
| 61 | +To navigate around the app, render a `Link` somewhere. |
| 62 | + |
| 63 | +```jsx |
| 64 | +render( |
| 65 | + <div> |
| 66 | + <nav> |
| 67 | + <Link to="/">Home</Link> | <Link to="/dashboard">Dashboard</Link> |
| 68 | + </nav> |
| 69 | + <Router> |
| 70 | + <Home path="/" /> |
| 71 | + <Dash path="dashboard" /> |
| 72 | + </Router> |
| 73 | + </div> |
| 74 | +); |
| 75 | +``` |
| 76 | + |
| 77 | +### Parse data from the URL |
| 78 | + |
| 79 | +If you need to parse the data out of the URL, use a dynamic segment--they start with a `:`. The parsed value will become a prop sent to the matched component. |
| 80 | + |
| 81 | +```jsx |
| 82 | +// at url "/23" |
| 83 | + |
| 84 | +render( |
| 85 | + <Router> |
| 86 | + <Home path="/" /> |
| 87 | + <Invoice path=":invoiceId" /> |
| 88 | + </Router> |
| 89 | +); |
| 90 | + |
| 91 | +const Invoice = props => ( |
| 92 | + <div> |
| 93 | + <h1>Invoice {props.invoiceId}</h1> |
| 94 | + </div> |
| 95 | +); |
| 96 | +``` |
| 97 | + |
| 98 | +It's the same as rendering the component directly. |
| 99 | + |
| 100 | +```jsx |
| 101 | +<Invoice invoiceId={23} /> |
| 102 | +``` |
| 103 | + |
| 104 | +### Ambiguous paths and ranking |
| 105 | + |
| 106 | +Even though two paths might be ambiguous--like "/:invoiceId" and "/invoices"--Router ranks the paths and renders the one that makes the most sense. |
| 107 | + |
| 108 | +```jsx |
| 109 | +render( |
| 110 | + <Router> |
| 111 | + <Home path="/" /> |
| 112 | + <Invoice path=":invoiceId" /> |
| 113 | + <InvoiceList path="invoices" /> |
| 114 | + </Router> |
| 115 | +); |
| 116 | +``` |
| 117 | + |
| 118 | +The URL "/invoices" will render `<Invoices/>` and "/123" will render `<Invoice invoiceId={123}/>`. Same thing with the `Home` component. Even though it’s defined first, and every path will match "/", `Home` won't render unless the path is exactly "/". So don't worry about the order of your paths. |
| 119 | + |
| 120 | +### Nested Router children and paths |
| 121 | + |
| 122 | +You can nest components inside of a Router, and the paths will nest too. The matched child component will come in as the `children` prop, the same as if you'd rendered it directly. (Internally `Router` just renders another `Router` with a `basepath`, but I digress...) |
| 123 | + |
| 124 | +```jsx |
| 125 | +const Dash = ({ children }) => ( |
| 126 | + <div> |
| 127 | + <h1>Dashboard</h1> |
| 128 | + <hr /> |
| 129 | + {children} |
| 130 | + </div> |
| 131 | +); |
| 132 | + |
| 133 | +render( |
| 134 | + <Router> |
| 135 | + <Home path="/" /> |
| 136 | + <Dash path="dashboard"> |
| 137 | + <Invoices path="invoices" /> |
| 138 | + <Team path="team" /> |
| 139 | + </Dash> |
| 140 | + </Router> |
| 141 | +); |
| 142 | +``` |
| 143 | + |
| 144 | +If the URL is "/dashboard/invoices" then the Router will render `<Dash><Invoices/></Dash>`. If it's just "/dashboard", `children` will be `null` and we’ll only see `<Dash/>`. |
| 145 | + |
| 146 | +### Relative Links |
| 147 | + |
| 148 | +You can link to relative paths. The relativity comes from the path of the component that rendered the Link. These two links will link to "/dashboard/invoices" and "/dashboard/team" because they're rendered inside of `<Dash/>`. This is really nice when you change a parent's URL, or move the components around, there’s no need to change the links. |
| 149 | + |
| 150 | +```jsx |
| 151 | +render( |
| 152 | + <Router> |
| 153 | + <Home path="/" /> |
| 154 | + <Dash path="dashboard"> |
| 155 | + <Invoices path="invoices" /> |
| 156 | + <Team path="team" /> |
| 157 | + </Dash> |
| 158 | + </Router> |
| 159 | +); |
| 160 | + |
| 161 | +const Dash = ({ children }) => ( |
| 162 | + <div> |
| 163 | + <h1>Dashboard</h1> |
| 164 | + <nav> |
| 165 | + <Link to="invoices">Invoices</Link> <Link to="team">Team</Link> |
| 166 | + </nav> |
| 167 | + <hr /> |
| 168 | + {children} |
| 169 | + </div> |
| 170 | +); |
| 171 | +``` |
| 172 | + |
| 173 | +This also makes it trivial to render any section of your app as its own application. |
| 174 | + |
| 175 | +### "Index" paths |
| 176 | + |
| 177 | +Nested components can use the path `/` to signify they should render |
| 178 | +at the path of the parent component, like an index.html file inside |
| 179 | +a folder on a static server. If this app was at "/dashboard" we'd see this |
| 180 | +component tree: `<Dash><DashboardGraphs/></Dash>` |
| 181 | + |
| 182 | +```jsx |
| 183 | +render( |
| 184 | + <Router> |
| 185 | + <Home path="/" /> |
| 186 | + <Dash path="dashboard"> |
| 187 | + <DashboardGraphs path="/" /> |
| 188 | + <InvoiceList path="invoices" /> |
| 189 | + </Dash> |
| 190 | + </Router> |
| 191 | +); |
| 192 | +``` |
| 193 | + |
| 194 | +### Not found "default" components |
| 195 | + |
| 196 | +Put a default prop on a component and Router will render it when nothing else matches. |
| 197 | + |
| 198 | +```jsx |
| 199 | +const NotFound = () => <div>Sorry, nothing here.</div>; |
| 200 | + |
| 201 | +render( |
| 202 | + <Router> |
| 203 | + <Home path="/" /> |
| 204 | + <Dash path="dashboard"> |
| 205 | + <DashboardGraphs path="/" /> |
| 206 | + <InvoiceList path="invoices" /> |
| 207 | + </Dash> |
| 208 | + <NotFound default /> |
| 209 | + </Router> |
| 210 | +); |
| 211 | +``` |
| 212 | + |
| 213 | +### Multiple Routers |
| 214 | + |
| 215 | +If you want to match the same path in two places in your app, just render two |
| 216 | +Routers. Again, a Router picks a single child to render based on the URL, and |
| 217 | +then ignores the rest. |
| 218 | + |
| 219 | +```jsx |
| 220 | +render( |
| 221 | + <div> |
| 222 | + <Sidebar> |
| 223 | + <Router> |
| 224 | + <HomeNav path="/" /> |
| 225 | + <DashboardNav path="dashboard" /> |
| 226 | + </Router> |
| 227 | + </Sidebar> |
| 228 | + |
| 229 | + <MainScreen> |
| 230 | + <Router> |
| 231 | + <Home path="/"> |
| 232 | + <About path="about" /> |
| 233 | + <Support path="support" /> |
| 234 | + </Home> |
| 235 | + <Dash path="dashboard"> |
| 236 | + <Invoices path="invoices" /> |
| 237 | + <Team path="team" /> |
| 238 | + </Dash> |
| 239 | + </Router> |
| 240 | + </MainScreen> |
| 241 | + </div> |
| 242 | +); |
| 243 | +``` |
| 244 | + |
| 245 | +### Nested Routers |
| 246 | + |
| 247 | +You can render a router anywhere you want in your app, even deep inside another Router. All the matching and linking will be relative to all the parents. |
| 248 | + |
| 249 | +```jsx |
| 250 | +render( |
| 251 | + <Router> |
| 252 | + <Home path="/" /> |
| 253 | + <Dash path="dashboard" /> |
| 254 | + </Router> |
| 255 | +); |
| 256 | + |
| 257 | +const Dash = () => ( |
| 258 | + <div> |
| 259 | + <p>A nested router</p> |
| 260 | + <Router> |
| 261 | + <DashboardGraphs path="/" /> |
| 262 | + <InvoiceList path="invoices" /> |
| 263 | + </Router> |
| 264 | + </div> |
| 265 | +); |
| 266 | +``` |
| 267 | + |
| 268 | +This allows you to have all of your routes configured at the top of the app, or to configure only where you need them, which is really helpful for code-splitting and very large apps. |
| 269 | + |
| 270 | +### Navigating programmatically |
| 271 | + |
| 272 | +If you need to navigate programmatically (like after a form submits) |
| 273 | +use the `navigate` prop that comes to your component |
| 274 | + |
| 275 | +```jsx |
| 276 | +const Invoices = ({ navigate }) => ( |
| 277 | + <div> |
| 278 | + <NewInvoiceForm |
| 279 | + onSubmit={async event => { |
| 280 | + const newInvoice = await createInvoice(event.target); |
| 281 | + navigate(`/invoice/${newInvoice.id}`); |
| 282 | + }} |
| 283 | + /> |
| 284 | + </div> |
| 285 | +); |
| 286 | +``` |
| 287 | + |
| 288 | +Or get it from context if you're deep in the render tree. |
| 289 | + |
| 290 | +```jsx |
| 291 | +const NotARouterChildComponent = () => ( |
| 292 | + <div> |
| 293 | + <p>Somewhere deep</p> |
| 294 | + <Location> |
| 295 | + {({ navigate }) => ( |
| 296 | + <NewInvoiceForm |
| 297 | + onSubmit={async event => { |
| 298 | + const newInvoice = await createInvoice(event.target); |
| 299 | + navigate(`/invoice/${newInvoice.id}`); |
| 300 | + }} |
| 301 | + /> |
| 302 | + )} |
| 303 | + </Location> |
| 304 | + </div> |
| 305 | +); |
| 306 | +``` |
| 307 | + |
| 308 | +Navigate returns a promise so you can await it. It resolves after React is completely finished rendering the next screen. |
| 309 | + |
| 310 | +```jsx |
| 311 | +class Invoices extends React.Component { |
| 312 | + state = { |
| 313 | + creatingNewInvoice: false |
| 314 | + }; |
| 315 | + |
| 316 | + render() { |
| 317 | + const { navigate } = this.props; |
| 318 | + return ( |
| 319 | + <div> |
| 320 | + <LoadingBar animate={this.state.creatingNewInvoice} /> |
| 321 | + <NewInvoiceForm |
| 322 | + onSubmit={async event => { |
| 323 | + this.setState({ creatingNewInvoice: true }); |
| 324 | + const newInvoice = await createInvoice(event.target); |
| 325 | + await navigate(`/invoice/${newInvoice.id}`); |
| 326 | + this.setState({ creatingNewInvoice: false }); |
| 327 | + }} |
| 328 | + /> |
| 329 | + <InvoiceList /> |
| 330 | + </div> |
| 331 | + ); |
| 332 | + } |
| 333 | +} |
| 334 | +``` |
| 335 | + |
| 336 | +## React Suspense and Time Slicing Ready |
| 337 | + |
| 338 | +### History stack handling |
| 339 | + |
| 340 | +With React Suspense a user may click on a link, and while data is loading they may change their mind and click on a different link. The browser navigation history will contain all three entries: |
| 341 | + |
| 342 | +``` |
| 343 | +/first-page -> /cancelled-page -> /desired-page |
| 344 | +``` |
| 345 | + |
| 346 | +If they click the back button from `/desired-page`, they'll get an unexpected `/cancelled-paged` screen. They didn't see it on their way to `/desired-page` so it doesn’t make sense for it to be there on the way back to `/first-page`. |
| 347 | + |
| 348 | +Reactions Router will not add the cancelled page to the history stack, so it would look like this: |
| 349 | + |
| 350 | +``` |
| 351 | +/first-page -> /desired-page |
| 352 | +``` |
| 353 | + |
| 354 | +Now when the user clicks back, they don’t end up on a page they never even saw. This is how browsers work with plain HTML pages, too. |
| 355 | + |
| 356 | +### Low priority updates |
| 357 | + |
| 358 | +Router takes advantage of "Time Slicing" in React . It's very common to hook a user input up to a query string in the URL. Every time the user types, the url updates, and then React rerenders. Router state is given "low priority" so these inputs will not bind the CPU like they would have otherwise. |
| 359 | + |
| 360 | +## Legal |
| 361 | + |
| 362 | +MIT License |
| 363 | +Copyright (c) 2018-present, Ryan Florence |
0 commit comments