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.
- package/index.min.js +1 -1
- package/package.json +2 -2
- package/readme.md +360 -39
package/index.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
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.
|
|
4
|
-
"description": "A React library to handle navigation in your
|
|
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 [](https://www.npmjs.com/package/crossroad) [](https://github.com/franciscop/crossroad/blob/master/.github/workflows/tests.yml) [](https://github.com/franciscop/crossroad/blob/master/index.min.js)
|
|
2
2
|
|
|
3
|
-
A React library to handle navigation in your
|
|
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
|
-
-
|
|
6
|
-
-
|
|
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
|
-
|
|
336
|
-
setUrl(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
setUrl({
|
|
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
|
-
|
|
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
|
-
|
|
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("/
|
|
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
|
-
|
|
381
|
-
const [query, setQuery] = useQuery();
|
|
382
|
-
// { search: 'name', filter: 'new' }
|
|
410
|
+
import { useQuery } from "crossroad";
|
|
383
411
|
|
|
384
|
-
|
|
385
|
-
//
|
|
412
|
+
export default function SearchInput() {
|
|
413
|
+
// In /users?search=
|
|
414
|
+
const [query, setQuery] = useQuery();
|
|
415
|
+
// [{ search: "" }, fn]
|
|
386
416
|
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
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
|
-
|
|
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" });
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
456
|
+
const [myname, setMyname] = useQuery("myname");
|
|
457
|
+
|
|
458
|
+
// ...
|
|
459
|
+
|
|
460
|
+
setMyname(newName || null);
|
|
419
461
|
```
|
|
420
462
|
|
|
421
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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.
|