crossroad 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.min.js +1 -1
  2. package/package.json +2 -2
  3. package/readme.md +360 -39
package/index.min.js CHANGED
@@ -1 +1 @@
1
- import e,{createContext as t,useContext as r,useState as n,useEffect as o}from"react";const i=e=>{if("string"!=typeof e)return e;const t={},r=new URL(e,"http://localhost:3000/");var n;(t.path=r.pathname.replace(/\/$/,"")||"/",r.hash)&&(t.hash=null===(n=r.hash)||void 0===n?void 0:n.replace(/^#/,""));t.query={};for(const[e,n]of r.searchParams)t.query[e]=n;return t};function a(e,t,r={}){if(e=JSON.parse(JSON.stringify(i(e))),(t=JSON.parse(JSON.stringify(i(t)))).path.endsWith("/")&&(t.path=t.path.slice(0,-1)||"/"),e.path.endsWith("/")&&(e.path=e.path.slice(0,-1)||"/"),e.path.endsWith("*")){e.path=e.path.replace(/\/?\*/,"")||"/";const r=e.path.slice(1).split("/").filter(Boolean).length;t.path="/"+t.path.slice(1).split("/").slice(0,r).join("/")}if(Object.entries(e.query).length)for(let r in e.query){if(!(r in t.query))return!1;if(e.query[r]&&e.query[r]!==t.query[r])return!1}if(t.path===e.path)return!0;if(!e.path.includes(":"))return e.path===t.path;if(e.path.split("/").length!==t.path.split("/").length)return!1;const n={},o=e.path.split("/").every((e,r)=>{const o=t.path.split("/")[r];return e.startsWith(":")?(n[e.slice(1)]=o,!0):o===e});return o&&Object.assign(r,n),o}var s=t();const l=()=>{const e=r(s);if(!e)throw new Error("Hooks should be used as children of <Router>");return e},u=()=>{const[e,t]=l();return[e.path,(r,n)=>t({...e,path:r},n)]},p=e=>{const[t,r]=l(),n=t.query,o=(e,n)=>r({...t,query:e},n);return e?[n[e],(t,r)=>o({...n,[e]:t},r)]:[n,o]},c=()=>{const[e,t]=l();return[e.hash,(r,n)=>t({...e,hash:r},n)]},h=e=>{const t=r(s),[n]=u();if(e){const t={};return a(e,n,t),t}return t[0].params},d=({path:t="*",exact:n=!0,component:o,render:i,children:u})=>{const p=r(s),[c,h]=l(),d={};if(!a(t,c,d))return null;const f=[{...p[0],params:d},p[1]];if(o){const t=o;return e.createElement(s.Provider,{value:f},e.createElement(t,d))}if(i)return e.createElement(s.Provider,{value:f},i(d));if(u)return e.createElement(s.Provider,{value:f},u);throw new Error("Route needs the prop `component`, `render` or `children`")},f=({redirect:e,children:t})=>{const[r,n]=l(),i=(e=>e?Array.isArray(e)?[...e]:[e]:[])(t).find(e=>a(e.props.path||"*",r))||null;return o(()=>{e&&!i&&n(e)},[e,Boolean(i)]),i};export default({url:t,children:r})=>{const a="undefined"==typeof window?"/":window.location.href,[l,u]=n(i(t||a)),p=(e,t={mode:"push"})=>{"string"==typeof e&&(e=i(e));const r=(({path:e,query:t,hash:r}={})=>{let n=e||"/";return t&&Object.keys(t).length&&(n+="?",n+=Object.entries(t).filter(([e,t])=>t||""===t).map(([e,t])=>e+"="+encodeURIComponent(t)).join("&")),r&&(n+="#"+r),n})(e);if(!["push","replace"].includes(t.mode))throw new Error(`Unrecognized mode "${t.mode}"`);"replace"===t.mode?history.replaceState({},null,r):history.pushState({},null,r),u(e)};return o(()=>{const e=e=>u(i(window.location.href)),t=e=>{const t=(e=>{if(!e)return null;const t=e.getAttribute("href");return t?/^https?:\/\//.test(t)||null!==e.getAttribute("target")?null:t:null})(e.target.closest("a"));t&&(e.preventDefault(),p(t))};return"undefined"!=typeof window&&window.addEventListener("popstate",e),"undefined"!=typeof document&&document.addEventListener("click",t),()=>{"undefined"!=typeof window&&window.removeEventListener("popstate",e),"undefined"!=typeof document&&document.removeEventListener("click",t)}},[]),e.createElement(s.Provider,{value:[l,p]},r)};export{s as Context,d as Route,f as Switch,c as useHash,h as useParams,u as usePath,p as useQuery,l as useUrl};
1
+ import t,{createContext as e,useState as r,useEffect as n,useContext as a}from"react";var o=e();const s=t=>{if("string"!=typeof t)return t;const e={},r=new URL(t,"http://localhost:3000/");e.path=r.pathname.replace(/\/$/,"")||"/",e.query={};for(const[t]of r.searchParams)e.query[t]=(n=r.searchParams.getAll(t)).length>1?n:n[0];var n;return r.hash&&(e.hash=r.hash.replace(/^#/,"")),e},i=t=>{if("string"==typeof t)return t;const{path:e,query:r={},hash:n}=t||{};let a=e||"/";const o=new URLSearchParams(Object.entries(r).map(([t,e])=>(e.map?e:[e]).map(e=>[t,e])).flat()).toString();return o&&(a+="?"+o),n&&(a+="#"+n),a};var p=()=>"undefined"==typeof window;function l(t,e,r={}){if(t=JSON.parse(JSON.stringify(s(t))),(e=JSON.parse(JSON.stringify(s(e)))).path=e.path.replace(/\/$/,"")||"/",t.path=t.path.replace(/\/$/,"")||"/",t.path.endsWith("*")){t.path=t.path.replace(/\/?\*/,"")||"/";const r=t.path.split("/").filter(Boolean).length;e.path="/"+e.path.slice(1).split("/").slice(0,r).join("/")}if(Object.entries(t.query).length)for(let r in t.query){if(!(r in e.query))return!1;if(t.query[r]&&t.query[r]!==e.query[r])return!1}if(!t.path.includes(":"))return t.path===e.path&&r;if(t.path.split("/").length!==e.path.split("/").length)return!1;const n={},a=t.path.split("/").every((t,a)=>{const o=e.path.split("/")[a];return t.startsWith(":")?(n[t.slice(1)]=o,r):o===t});return a&&Object.assign(r,n),a&&r}var h=({path:e="*",exact:r=!0,component:n,render:s,children:i})=>{const p=a(o),h=l(e,p[0]);if(!h)return null;if(n){const e=n;i=t.createElement(e,h)}else if(s)i=s(h);else if(!i)throw new Error("Route needs prop `component`, `render` or `children`");return t.createElement(o.Provider,{value:[{...p[0],params:h},...p.slice(1)]},i)},c=()=>{const t=a(o);if(!t)throw new Error("Wrap your App with <Router>");return t};var u=({redirect:t,children:e})=>{const[r,a]=c(),o=(t=>(Array.isArray(t)||(t=[t]),t.filter(t=>t&&t.props)))(e).find(t=>l(t.props.path||"*",r))||null;return n(()=>{t&&(o||("function"==typeof t&&(t=t(r)),a(i(t))))},[t,o]),o},f=()=>{const[t,e]=c();return[t.path,(r,n)=>e({...t,path:r},n)]},d=t=>{const[e,r]=c(),n=e.query,a=(t,n)=>r({...e,query:t},n);return t?[n[t],(e,r)=>a({...n,[t]:e},r)]:[n,a]},y=()=>{const[t,e]=c();return[t.hash,(r,n)=>e({...t,hash:r},n)]},m=t=>{const e=a(o);return t?l(t,e[0].path)||{}:e[0].params};export default({url:e,children:a})=>{const l=e||(p()?"/":window.location.href),[h,c]=r(()=>s(l)),u=(t,{mode:e="push"}={})=>{if(!history[e+"State"])throw new Error(`Invalid mode "${e}"`);history[e+"State"]({},null,i(t)),c(s(t))};return n(()=>{if(p())return;const t=()=>u(window.location.href),e=t=>{const e=(t=>{if(!t)return null;const e=t.getAttribute("href");return e?/^https?:\/\//.test(e)||null!==t.getAttribute("target")?null:e:null})(t.target.closest("a"));e&&(t.preventDefault(),u(e))};return window.addEventListener("popstate",t),document.addEventListener("click",e),()=>{window.removeEventListener("popstate",t),document.removeEventListener("click",e)}},[]),t.createElement(o.Provider,{value:[h,u]},a)};export{o as Context,h as Route,u as Switch,y as useHash,m as useParams,f as usePath,d as useQuery,c as useUrl};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "crossroad",
3
- "version": "0.14.0",
4
- "description": "A React library to handle navigation in your webapp. Built with simple components and React Hooks.",
3
+ "version": "0.16.0",
4
+ "description": "A React library to handle navigation in your WebApp. Built with simple components and React Hooks so your code is cleaner.",
5
5
  "homepage": "https://crossroad.page/",
6
6
  "repository": "https://github.com/franciscop/crossroad.git",
7
7
  "bugs": "https://github.com/franciscop/crossroad/issues",
package/readme.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # Crossroad [![npm install crossroad](https://img.shields.io/badge/npm%20install-crossroad-blue.svg "install badge")](https://www.npmjs.com/package/crossroad) [![test badge](https://github.com/franciscop/crossroad/workflows/tests/badge.svg "test badge")](https://github.com/franciscop/crossroad/blob/master/.github/workflows/tests.yml) [![gzip size](https://img.badgesize.io/franciscop/crossroad/master/index.min.js.svg?compression=gzip "gzip badge")](https://github.com/franciscop/crossroad/blob/master/index.min.js)
2
2
 
3
- A React library to handle navigation in your webapp. Built with simple components and React Hooks. It has [some differences](#react-router-differences) with React Router so you write cleaner code:
3
+ A React library to handle navigation in your WebApp. Built with simple components and React Hooks so you write cleaner code:
4
4
 
5
- - The links are plain `<a>` instead of custom components. [Read more](#a).
6
- - There are useful hooks like [`useUrl`](#useurl), [`useQuery`](#usequery), etc.
5
+ - `<Router>`, `<Switch>` and `<Route>` inspired by React Router so it's easy to get started.
6
+ - Very useful hooks like [`useUrl`](#useurl), [`useQuery`](#usequery), etc.
7
+ - Links are plain `<a>` instead of custom components. [Read more](#a).
7
8
  - The `<Route>` path is `exact` by default and can match query parameters.
8
9
  - It's [just ~1.5kb](https://bundlephobia.com/package/crossroad) (min+gzip) instead of the 17kb of React Router(+Dom).
9
10
 
@@ -146,6 +147,15 @@ You might want to redirect the user to a specific route (like `/notfound`) when
146
147
  </Switch>
147
148
  ```
148
149
 
150
+ The redirect parameter can be a plain string, an url-like object or a callback that returns any of the previous:
151
+
152
+ ```js
153
+ <Switch redirect="/gohere?hello=world"></Switch>
154
+ <Switch redirect={{ path: "/gohere", query: { hello: "world" } }}></Switch>
155
+ <Switch redirect={() => "/gohere"}></Switch>
156
+ <Switch redirect={url => ({ ...url, path: "/gohere" })}></Switch>
157
+ ```
158
+
149
159
  Or to keep it in the current route, whatever it is, you can render a component with no path (no path === `*`):
150
160
 
151
161
  ```js
@@ -292,6 +302,8 @@ Some examples:
292
302
  Read and set the full URL:
293
303
 
294
304
  ```js
305
+ import { useUrl } from "crossroad";
306
+
295
307
  export default function Login() {
296
308
  const [url, setUrl] = useUrl();
297
309
 
@@ -307,7 +319,6 @@ export default function Login() {
307
319
  These are the structures of each:
308
320
 
309
321
  - `url`: an object with the properties, it's similar to the native URL:
310
- - `url.href`: a string with the full URL (path + query + hash)
311
322
  - `url.path`: a string with the current pathname
312
323
  - `url.query`: an object with the keys and values. Example: `{ q: 'hello' }`, `{ q: 'hello', s: 'world' }`.
313
324
  - `url.hash`: the hashtag, without the "#"
@@ -332,21 +343,35 @@ You can also set it fully or partially:
332
343
  ```js
333
344
  const [url, setUrl] = useUrl();
334
345
 
335
- setUrl("/#firsttime"); // [Shorthand] Redirect to home with a hashtag
336
- setUrl({ path: "/", hash: "firsttime" }); // Same as above
337
- setUrl({ ...url, path: "/" }); // Keep everything the same except the path
338
- setUrl({ ...url, query: { search: myQuery } }); // Set a full search query
339
- setUrl({ ...url, query: { ...url.query, safe: 0 } }); // Modify only one query param
346
+ // [Shorthand] Redirect to home with a hashtag
347
+ setUrl("/#firsttime");
348
+
349
+ // Same as above, but specifying the parts
350
+ setUrl({ path: "/", hash: "firsttime" });
351
+
352
+ // Keep everything the same except the path
353
+ setUrl({ ...url, path: "/" });
354
+
355
+ // Set a full search query
356
+ setUrl({ ...url, query: { search: "hello" } });
357
+
358
+ // Modify only one query param
359
+ setUrl({ ...url, query: { ...url.query, safe: 0 } });
340
360
  ```
341
361
 
342
362
  `useUrl()` is powerful enough for all of your needs, but you might still be interested in other hooks to simplify situations where you do e.g. heavy query manipulation with `useQuery`.
343
363
 
344
- By default `setUrl()` will create a new entry in the browser history. If you want to instead replace the current entry you can pass a second parameter with `{ mode: 'replace' }`:
364
+ #### New history entry
365
+
366
+ By default `setUrl()` will create a new entry in the browser history. If you want to instead replace the current url you can pass a second parameter with `{ mode: 'replace' }`:
345
367
 
346
368
  ```js
347
369
  setUrl("/newurl", { mode: "replace" });
348
370
  ```
349
371
 
372
+ - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/c` and then click on the back button, the browser will go back to `/b`.
373
+ - `replace`: creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(replace)> `/c` and then click on the back button, it'll go back to `/a`. This is because `/c` is overwriting `/b`, instead of adding a new entry.
374
+
350
375
  ### `usePath()`
351
376
 
352
377
  Read and set only the path(name) part of the URL:
@@ -366,36 +391,44 @@ const Login = () => {
366
391
 
367
392
  > Note: this _only_ modifies the path(name) and keeps the search query and hash the same, so if you want to modify the full URL you should instead utilize `useUrl()` and `setUrl('/welcome')`
368
393
 
369
- By default `setPath()` will create a new entry in the browser history. If you want to instead replace the current entry you can pass a second parameter with `{ mode: 'replace' }`:
394
+ #### New history entry
395
+
396
+ By default `setPath()` will create a new entry in the browser history. If you want to instead replace the current url you can pass a second parameter with `{ mode: 'replace' }`:
370
397
 
371
398
  ```js
372
- setPath("/newurl", { mode: "replace" });
399
+ setPath("/newpath", { mode: "replace" });
373
400
  ```
374
401
 
402
+ - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/c` and then click on the back button, the browser will go back to `/b`.
403
+ - `replace`: creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(replace)> `/c` and then click on the back button, it'll go back to `/a`. This is because `/c` is overwriting `/b`, instead of adding a new entry.
404
+
375
405
  ### `useQuery()`
376
406
 
377
407
  Read and set only the search query parameters from the URL:
378
408
 
379
409
  ```js
380
- // In /users?search=name&filter=new
381
- const [query, setQuery] = useQuery();
382
- // { search: 'name', filter: 'new' }
410
+ import { useQuery } from "crossroad";
383
411
 
384
- setQuery({ search: "myname" }); // Remove the other query params
385
- // Goto /users?search=myname
412
+ export default function SearchInput() {
413
+ // In /users?search=
414
+ const [query, setQuery] = useQuery();
415
+ // [{ search: "" }, fn]
386
416
 
387
- setQuery({ ...query, search: "myname" }); // Keep the other query params
388
- // Goto /users?search=myname&filter=new
417
+ // Goes to /users?search={value}
418
+ const onChange = e => setQuery({ search: e.target.value });
419
+
420
+ return <input value={query.search} onChange={onChange} />;
421
+ }
389
422
  ```
390
423
 
391
- If you pass a parameter, it can read and modify that parameter while keeping the others the same. This is specially useful in e.g. a search form:
424
+ If you pass a key, it can read and modify that parameter while keeping the others the same. This is specially useful in e.g. a search form:
392
425
 
393
426
  ```js
394
427
  // In /users?search=name&filter=new
395
428
  const [search, setSearch] = useQuery("search");
396
429
  // 'name'
397
430
 
398
- setQuery("myname");
431
+ setSearch("myname");
399
432
  // Goto /users?search=myname&filter=new
400
433
  ```
401
434
 
@@ -405,30 +438,49 @@ When you update it, it will clean any parameter not passed, so make sure to pass
405
438
  // In /users?search=name&filter=new
406
439
  const [query, setQuery] = useQuery();
407
440
 
408
- setQuery({ search: "myname" }); // Goto /users?q=myname (removes the filter)
409
- setQuery({ ...query, search: "myname" }); // Goto /users?q=myname&filter=new
410
- setQuery(prev => ({ ...prev, search: "myname" })); // Goto /users?q=myname&filter=new
441
+ setQuery({ search: "myname" });
442
+ // Goto /users?q=myname (removes the filter)
443
+
444
+ setQuery({ ...query, search: "myname" });
445
+ // Goto /users?q=myname&filter=new
446
+
447
+ setQuery(prev => ({ ...prev, search: "myname" }));
448
+ // Goto /users?q=myname&filter=new
411
449
  ```
412
450
 
413
451
  `setQuery` only modifies the query string part of the URL, keeping the `path` and `hash` the same as they were previously.
414
452
 
415
- By default `setQuery()` will create a new entry in the browser history. If you want to instead replace the current entry, so that the "Back" button goes to the previous page, you can pass a second parameter with `{ mode: 'replace' }`:
453
+ When you set a search query to `null` or `false`, it will be removed from the URL. However, empty strings are not removed. So if you want empty strings to also remove the parameter in the URL, please do this:
416
454
 
417
455
  ```js
418
- setQuery({ search: "abc" }, { mode: "replace" });
456
+ const [myname, setMyname] = useQuery("myname");
457
+
458
+ // ...
459
+
460
+ setMyname(newName || null);
419
461
  ```
420
462
 
421
- When you set a search query to `null` or `false`, it will be removed from the URL. However, empty strings are not removed. So if you want empty strings to also remove the parameter in the URL, please do this:
463
+ If you are using `react-query` and already have a bunch of `useQuery()` in your code and prefer to use other name, just rename this method when importing it:
422
464
 
423
465
  ```js
424
- const [myname, setMyname] = useQuery("myname");
425
- // ...
426
- setWord(newName || null);
466
+ import { useQuery as useSearch } from 'crossroad';
467
+ ...
468
+ ```
469
+
470
+ #### New history entry
471
+
472
+ By default `setQuery()` will create a new entry in the browser history. If you want to instead replace the current entry, so that the "Back" button goes to the previous page, you can pass a second parameter with `{ mode: 'replace' }`:
473
+
474
+ ```js
475
+ setQuery({ search: "abc" }, { mode: "replace" });
427
476
  ```
428
477
 
478
+ - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/b?q=c` and then click on the back button, the browser will go back to `/b`.
479
+ - `replace`: creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(replace)> `/b?q=c` and then click on the back button, it'll go back to `/a`. This is because `/b?q=c` is overwriting `/b`, instead of adding a new entry.
480
+
429
481
  ### `useHash()`
430
482
 
431
- Read and set only the hash part of the URL:
483
+ Read and set only the hash part of the URL (without the `"#"`):
432
484
 
433
485
  ```js
434
486
  // In /login#welcome
@@ -445,6 +497,19 @@ By default `setHash()` will create a new entry in the browser history. If you wa
445
497
  setHash("newhash", { mode: "replace" });
446
498
  ```
447
499
 
500
+ If you want to remove the hash, pass a `null` or `undefined` to the setter.
501
+
502
+ #### New history entry
503
+
504
+ By default `setHash()` will create a new entry in the browser history. If you want to instead replace the current entry, so that the "Back" button goes to the previous page, you can pass a second parameter with `{ mode: 'replace' }`:
505
+
506
+ ```js
507
+ setHash("newhash", { mode: "replace" });
508
+ ```
509
+
510
+ - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/b#c` and then click on the back button, the browser will go back to `/b`.
511
+ - `replace`: creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(replace)> `/b#c` and then click on the back button, it'll go back to `/a`. This is because `/b#c` is overwriting `/b`, instead of adding a new entry.
512
+
448
513
  ### `useParams()`
449
514
 
450
515
  Parse the current URL against the given reference:
@@ -455,6 +520,8 @@ const params = useParams("/users/:id");
455
520
  // { id: '2' }
456
521
  ```
457
522
 
523
+ > Note: this returns a plain object, not a [value, setter] array
524
+
458
525
  It's not this method responsibility to match the url, just to attempt to parse it, so if there's no good match it'll just return an empty object (use a `<Route />` for path matching):
459
526
 
460
527
  ```js
@@ -592,14 +659,269 @@ In here we can see that we are treating the output of `useQuery` in the sam way
592
659
 
593
660
  ### Query routing
594
661
 
662
+ Some times you prefer the current page to be defined by the query, instead of by the pathname. This might be true for subpages, for tabs, or for other things depending on your app. With Crossroad it's easy to manage:
663
+
664
+ [**Codesandbox**](https://codesandbox.io/s/white-moon-5q0hr)
665
+
666
+ https://user-images.githubusercontent.com/2801252/132944863-3caf9399-d0c1-4cdc-86a0-dca1a6a4b4d1.mp4
667
+
668
+ ```js
669
+ <Switch redirect="/?page=home">
670
+ <Route path="/?page=home" component={Tabs.Home} />
671
+ <Route path="/?page=product" component={Tabs.Product} />
672
+ <Route path="/?page=about" component={Tabs.About} />
673
+ </Switch>
674
+ ```
675
+
676
+ With the code above, it will match the given component when the path is exactly "/" and the query parameter is the given one. If no one is matched, then it'll redirect you to `/?page=home`, the main page.
677
+
678
+ You can also use this for subpages, say if you were in a Dashboard:
679
+
680
+ ```js
681
+ <Switch redirect="/dashboard?tab=home">
682
+ <Route path="/dashboard?tab=home" component={Tabs.Home} />
683
+ <Route path="/dashboard?tab=product" component={Tabs.Product} />
684
+ <Route path="/dashboard?tab=about" component={Tabs.About} />
685
+ </Switch>
686
+ ```
687
+
595
688
  ### Not found
596
689
 
690
+ We have already seen in different examples how to do simple redirects with a single `<Switch redirect="">`, so now let's create a page for whenever the switch is not found:
691
+
692
+ ```js
693
+ <Switch>
694
+ <Route path="/" component={Home} />
695
+ <Route path="/users" component={Users} />
696
+
697
+ {/* Not found page */}
698
+ <Route component={NotFound} />
699
+ </Switch>
700
+ ```
701
+
702
+ This page will maintain the url in the browser, but render the NotFound component. Notice how we didn't write any `path=""`, omitting the `path` is the same as writing it as `path="*"`, which will catch everything.
703
+
704
+ So the way this Switch works here, it will try to match the URL against `"/"`, then against `"/users"`, and if it's none of those it'll match it against `"*"` (since that's always a match) and render the NotFound component.
705
+
706
+ We can also have different not found pages. Let's say we have a specific "documentation page not found" with helpful documentation links and a general one for the rest of the website, we can manage them this way then:
707
+
708
+ ```js
709
+ <Switch>
710
+ <Route path="/" component={Home} />
711
+ <Route path="/docs/abc" component={DocsAbc} />
712
+ <Route path="/docs/def" component={DocsDef} />
713
+
714
+ {/* Not found page only for the docs */}
715
+ <Route path="/docs/*" component={NotFoundDocs} />
716
+
717
+ {/* Not found page only for everything else */}
718
+ <Route component={NotFound} />
719
+ </Switch>
720
+ ```
721
+
722
+ In this case the order matters, because the generic NotFound will be matched with any route (since it's `"*"`), so we need to match first the docs that is not found and then, even if that fails (e.g. on the path `/hello`) we can render the generic NotFound component.
723
+
597
724
  ### Github hosting
598
725
 
599
- > NOTE: this is a bad idea for SEO, but if that doesn't matter much for you...
726
+ > NOTE: this is a bad idea for SEO, but if that doesn't matter much for you go ahead and host your webapp in Github Pages
727
+
728
+ Github pages is a bit particular in that as of this writing it does not allow for a generic redirect like most other static website servers, so we need to do a workaround with the `404.html` page.
729
+
730
+ This is because any of your visitors landing on `https://example.com/` will see the proper website (since that'll be directed to `docs/index.html`), but when the user lands on other paths like `https://example.com/info` it'll not find `docs/info.html` and thus render `404.html`.
731
+
732
+ So let's save the url and setup a redirect in `404.html`:
733
+
734
+ ```html
735
+ <!-- 404.html -->
736
+ <!DOCTYPE html>
737
+ <html lang="en">
738
+ <head>
739
+ <meta charset="UTF-8" />
740
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
741
+ <meta http-equiv="X-UA-Compatible" content="ie=edge" />
742
+ <title>Redirecting...</title>
743
+ </head>
744
+ <body>
745
+ <script>
746
+ const url = JSON.stringify(location.pathname + location.search);
747
+ localStorage.url = url;
748
+ location.replace("/");
749
+ </script>
750
+ </body>
751
+ </html>
752
+ ```
753
+
754
+ Then in your index.html, or in almost anywhere else, you can overwrite the URL:
755
+
756
+ ```js
757
+ if (localStorage.url) {
758
+ history.replaceState({}, null, JSON.decode(localStorage.url));
759
+ delete localStorage.url;
760
+ }
761
+ ```
600
762
 
601
763
  ### Testing routes
602
764
 
765
+ When testing a route, we can do it mainly in two different ways. The recommended one in general is that you pass a `url` prop straight into your `<Router>` component, which will force the Router to behave like the browser is in that route.
766
+
767
+ Let's see first a very simple App example, noting that for this case we are passing the `url` from App to Router:
768
+
769
+ ```js
770
+ // App.js
771
+ import Router, { Switch, Route } from "crossroad";
772
+
773
+ // Imagine these are your apps and components:
774
+ const Home = () => <div>Home</div>;
775
+ const Users = () => <div>Users</div>;
776
+ const NotFound = () => <div>Website not found</div>;
777
+
778
+ export default function App({ url }) {
779
+ return (
780
+ <Router url={url}>
781
+ <Switch>
782
+ <Route path="/" component={Home} />
783
+ <Route path="/users" component={Users} />
784
+ <Route component={NotFound} />
785
+ </Switch>
786
+ </Router>
787
+ );
788
+ }
789
+ ```
790
+
791
+ How does it work? The `url` prop will be undefined when a user loads the app (since we **don't** add it to index.js), so it is only being written for testing. On the users' browser, since it's undefinde Crossroad will use `window.location.href` instead.
792
+
793
+ ```js
794
+ // App.test.js
795
+ import React from "react";
796
+ import $ from "react-test";
797
+
798
+ import App from "./App";
799
+
800
+ describe("use the url prop", () => {
801
+ it("renders the home component on /", () => {
802
+ const $home = $(<App url="/" />);
803
+ expect($home.text()).toBe("Home");
804
+ });
805
+
806
+ it("renders the user list on /users", () => {
807
+ const $home = $(<App url="/users" />);
808
+ expect($home.text()).toBe("Users");
809
+ });
810
+
811
+ it("renders not found when in another route", () => {
812
+ const $home = $(<App url="/bla" />);
813
+ expect($home.text()).toContain("not found");
814
+ });
815
+ });
816
+ ```
817
+
818
+ This method is the simplest to get started, but some people don't like having to add code to the production website only for the testing environment. That's all fine, there's another way that is a bit harder to setup but it's also more accurate to the browser's real behavior.
819
+
820
+ #### Mock window.location
821
+
822
+ The previous method has **a big limitation**: it doesn't allow you to navigate within your app for a test, since it's always forcing the same url. To avoid this and be able to test better the real-world behavior, use this method.
823
+
824
+ When you are running Jest, it creates a fake `window` already, so you can plug into that to mock the behavior for the duration of the test. Doing it with a React component makes it even smoother:
825
+
826
+ ```js
827
+ // Mock.js
828
+ import React, { useEffect } from "react";
829
+
830
+ export default function Mock({ url, children }) {
831
+ const href = "http://localhost:3000" + url;
832
+ const oldLocation = { value: window.location };
833
+ delete global.window.location;
834
+ Object.defineProperty(global.window, "location", {
835
+ value: new URL(href),
836
+ configurable: true
837
+ });
838
+
839
+ // Undo the setup when the component unmounts
840
+ useEffect(() => {
841
+ return () => Object.defineProperty(window, "location", oldLocation);
842
+ });
843
+ return <div>{children}</div>;
844
+ }
845
+ ```
846
+
847
+ With this Mock component, then you can wrap your normal application into working with routes:
848
+
849
+ ```js
850
+ import React from "react";
851
+ import $ from "react-test";
852
+
853
+ import App from "./App";
854
+ import Mock from "./Mock";
855
+
856
+ describe("use the Mock component", () => {
857
+ it("renders the home component on /", () => {
858
+ const $home = $(
859
+ <Mock url="/">
860
+ <App />
861
+ </Mock>
862
+ );
863
+ expect($home.text()).toBe("Home");
864
+ });
865
+
866
+ it("renders the user list on /users", () => {
867
+ const $home = $(
868
+ <Mock url="/users">
869
+ <App />
870
+ </Mock>
871
+ );
872
+ expect($home.text()).toBe("Users");
873
+ });
874
+
875
+ it("renders not found when in another route", () => {
876
+ const $home = $(
877
+ <Mock url="/bla">
878
+ <App />
879
+ </Mock>
880
+ );
881
+ expect($home.text()).toContain("not found");
882
+ });
883
+ });
884
+ ```
885
+
886
+ ### Server Side Render
887
+
888
+ Crossroad has been tested with these libraries/frameworks for SSR:
889
+
890
+ - ✅ [Razzle](https://razzlejs.org/): it works adding a bit of config; Razzle bundles React Router Dom by default, so you need to install Crossroad, remove React Router Dom and add the code mentioned below.
891
+ - ⚠️ [Next.js](https://nextjs.org/): it works, but is generally not needed since Next.js include its own router and file-based routing.
892
+ - ❌ [Babel-Node](https://babeljs.io/docs/en/babel-node): BabelNode [doesn't support ECMAScript modules (ESM)](https://babeljs.io/docs/en/babel-node#es6-style-module-loading-may-not-function-as-expected), but you are **also** [not supposed to use `babel-node` for production anyway](https://babeljs.io/docs/en/babel-node#not-meant-for-production-use) so this is not a real framework for SSR.
893
+ - Others? I couldn't find many other ways that people are running SSR that I could test.
894
+
895
+ For Razzle (based on [these docs FaQ](https://razzlejs.org/docs/customization#transpilation-of-external-modules)):
896
+
897
+ ```js
898
+ // razzle.config.js
899
+ module.exports = {
900
+ modifyWebpackOptions({ options: { webpackOptions } }) {
901
+ webpackOptions.notNodeExternalResMatch = req => /crossroad/.test(req);
902
+ webpackOptions.babelRule.include.push(/crossroad/);
903
+ return webpackOptions;
904
+ }
905
+ };
906
+ ```
907
+
908
+ When working on the server, and similar to [how we saw in testing](#testing-routes), we can overload the current url:
909
+
910
+ ```js
911
+ // An express example
912
+ const App = ({ url }) => <Router url={url}>...</Router>;
913
+
914
+ app.get("/users", (req, res) => {
915
+ res.render(<App url="/users" />);
916
+ });
917
+
918
+ app.get("/users/:id", (req, res) => {
919
+ // {...} validate the `id` it here!
920
+
921
+ res.render(<App url={req.url} />);
922
+ });
923
+ ```
924
+
603
925
  ## React Router diff
604
926
 
605
927
  This part of the documentation tries to explain in detail the differences between Crossroad and React Router (Dom). Crossroad goal is to build a modern Router API from scratch, removing the legacy code and using Hooks natively.
@@ -675,6 +997,12 @@ const [search, setSearch] = useQuery("search");
675
997
  setSearch("myname2");
676
998
  ```
677
999
 
1000
+ See a video and a demo of how useful and easy it is to use `useQuery()`:
1001
+
1002
+ [**Codesandbox demo**](https://codesandbox.io/s/festive-murdock-1ctv6?file=/src/SearchForm.js)
1003
+
1004
+ https://user-images.githubusercontent.com/2801252/132189338-e09aa220-b773-43ed-803b-fa6c7449bf44.mp4
1005
+
678
1006
  ### Plain Links
679
1007
 
680
1008
  To add a link in your application, you use the native `<a>` element instead of having to import a different component. What's more, this makes links a lot more consistent than in React Router. Some examples:
@@ -710,10 +1038,3 @@ The same in React Router are like this, note the inconsistencies of some times u
710
1038
  <a href="https://example.com/" target="_blank">Hello</Link>
711
1039
  <Link to="https://example.com/">Hello</Link> // Broken
712
1040
  ```
713
-
714
- ## TODO
715
-
716
- This library is pretty much complete, but as always help with the documentation and test would be greatly appreciated. In particular:
717
-
718
- - There are some skipped tests because they are logging to the console and I haven't found a way to avoid that, see `src/index.test.js` and search for "skip".
719
- - More examples would be amazing.