Skip to content

Commit 56d4dca

Browse files
authored
Add Hooks APIs (#346)
* Add Hooks APIs * Documenation & change useMatch response to match Match * add hook API navigation links * some hooks documentation in the intro * address comments * yarn format
1 parent 15298df commit 56d4dca

File tree

14 files changed

+416
-114
lines changed

14 files changed

+416
-114
lines changed

examples/crud/src/App.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,8 @@ const Create = withInvalidateContacts(
256256
</ul>
257257
{state === CreateStates.ERROR && (
258258
<p>
259-
There was an error:<br />
259+
There was an error:
260+
<br />
260261
<br />
261262
<b>{error}</b>
262263
</p>

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@
5454
"prettier": "^1.13.4",
5555
"pretty-bytes": "^4.0.2",
5656
"pretty-quick": "^1.6.0",
57-
"react": "16.4.1",
58-
"react-dom": "16.4.2",
59-
"react-test-renderer": "16.4",
57+
"react": "16.12.0",
58+
"react-dom": "16.12.0",
59+
"react-test-renderer": "16.12",
6060
"render-markdown-js": "^1.3.0",
6161
"rollup": "^0.56.3",
6262
"rollup-plugin-babel": "^3.0.3",

src/__snapshots__/index.test.js.snap

+52
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,58 @@ exports[`disrespect has complete disrespect for leading and trailing slashes 1`]
9191
</div>
9292
`;
9393

94+
exports[`hooks useLocation returns the location 1`] = `
95+
<div
96+
style={
97+
Object {
98+
"outline": "none",
99+
}
100+
}
101+
tabIndex="-1"
102+
>
103+
path: /this/path/is/returned
104+
</div>
105+
`;
106+
107+
exports[`hooks useNavigate navigates relative 1`] = `
108+
<div
109+
style={
110+
Object {
111+
"outline": "none",
112+
}
113+
}
114+
tabIndex="-1"
115+
>
116+
IF_THIS_IS_IN_SNAPSHOT_BAAAAADDDDDDDD
117+
</div>
118+
`;
119+
120+
exports[`hooks useNavigate navigates relative 2`] = `
121+
<div
122+
style={
123+
Object {
124+
"outline": "none",
125+
}
126+
}
127+
tabIndex="-1"
128+
>
129+
THIS_IS_WHAT_WE_WANT_TO_SEE_IN_SNAPSHOT
130+
</div>
131+
`;
132+
133+
exports[`hooks useParams gives an object of the params from the route 1`] = `
134+
<div
135+
style={
136+
Object {
137+
"outline": "none",
138+
}
139+
}
140+
tabIndex="-1"
141+
>
142+
{"bar":"123","bax":"hi"}
143+
</div>
144+
`;
145+
94146
exports[`links renders links with relative hrefs 1`] = `
95147
<div
96148
style={

src/index.js

+56-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable jsx-a11y/anchor-has-content */
2-
import React from "react";
2+
import React, { useContext } from "react";
33
import PropTypes from "prop-types";
44
import invariant from "invariant";
55
import createContext from "create-react-context";
@@ -520,6 +520,56 @@ let Match = ({ path, children }) => (
520520
</BaseContext.Consumer>
521521
);
522522

523+
////////////////////////////////////////////////////////////////////////////////
524+
// Hooks
525+
526+
const useLocation = () => {
527+
const context = useContext(LocationContext);
528+
529+
if (!context) {
530+
throw new Error(
531+
"useLocation hook was used but a LocationContext.Provider was not found in the parent tree. Make sure this is used in a component that is a child of Router"
532+
);
533+
}
534+
535+
return context.location;
536+
};
537+
538+
const useNavigate = () => {
539+
const context = useContext(LocationContext);
540+
541+
return context.navigate;
542+
};
543+
544+
const useParams = () => {
545+
const { basepath } = useContext(BaseContext);
546+
const location = useLocation();
547+
548+
const results = match(basepath, location.pathname);
549+
550+
return results ? results.params : null;
551+
};
552+
553+
const useRouterMatch = path => {
554+
if (!path) {
555+
throw new Error(
556+
"useRouterMatch(path: string) requires an argument of a string to match against"
557+
);
558+
}
559+
const { baseuri } = useContext(BaseContext);
560+
const location = useLocation();
561+
562+
const resolvedPath = resolve(path, baseuri);
563+
const result = match(resolvedPath, location.pathname);
564+
return result
565+
? {
566+
...result.params,
567+
uri: result.uri,
568+
path
569+
}
570+
: null;
571+
};
572+
523573
////////////////////////////////////////////////////////////////////////////////
524574
// Junk
525575
let stripSlashes = str => str.replace(/(^\/+|\/+$)/g, "");
@@ -589,5 +639,9 @@ export {
589639
navigate,
590640
redirectTo,
591641
globalHistory,
592-
match as matchPath
642+
match as matchPath,
643+
useLocation,
644+
useNavigate,
645+
useParams,
646+
useRouterMatch
593647
};

src/index.test.js

+125-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import {
1414
Match,
1515
Redirect,
1616
isRedirect,
17-
ServerLocation
17+
ServerLocation,
18+
useLocation,
19+
useNavigate,
20+
useParams,
21+
useRouterMatch
1822
} from "./index";
1923

2024
let snapshot = ({ pathname, element }) => {
@@ -876,3 +880,123 @@ describe("trailing wildcard", () => {
876880
});
877881
});
878882
});
883+
884+
describe("hooks", () => {
885+
describe("useLocation", () => {
886+
it("returns the location", () => {
887+
function Fixture() {
888+
const location = useLocation();
889+
return `path: ${location.pathname}`;
890+
}
891+
892+
snapshot({
893+
pathname: `/this/path/is/returned`,
894+
element: (
895+
<Router>
896+
<Fixture path="/this/path/is/returned" />
897+
</Router>
898+
)
899+
});
900+
});
901+
902+
it("throws an error if a location context hasnt been rendered", () => {
903+
function Fixture() {
904+
const location = useLocation();
905+
return `path: ${location.pathname}`;
906+
}
907+
908+
expect(() => {
909+
renderToString(<Fixture />);
910+
}).toThrow(
911+
"useLocation hook was used but a LocationContext.Provider was not found in the parent tree. Make sure this is used in a component that is a child of Router"
912+
);
913+
});
914+
});
915+
916+
describe("useNavigate", () => {
917+
it("navigates relative", async () => {
918+
let navigate;
919+
920+
const Foo = () => {
921+
navigate = useNavigate();
922+
return `IF_THIS_IS_IN_SNAPSHOT_BAAAAADDDDDDDD`;
923+
};
924+
925+
const Bar = () => `THIS_IS_WHAT_WE_WANT_TO_SEE_IN_SNAPSHOT`;
926+
927+
const { snapshot } = runWithNavigation(
928+
<Router>
929+
<Foo path="/foo" />
930+
<Bar path="/bar" />
931+
</Router>,
932+
"/foo"
933+
);
934+
snapshot();
935+
await navigate("/bar");
936+
snapshot();
937+
});
938+
});
939+
940+
describe("useParams", () => {
941+
it("gives an object of the params from the route", () => {
942+
const Fixture = () => {
943+
const params = useParams();
944+
return JSON.stringify(params);
945+
};
946+
947+
snapshot({
948+
pathname: "/foo/123/baz/hi",
949+
element: (
950+
<Router>
951+
<Fixture path="/foo/:bar/baz/:bax" />
952+
</Router>
953+
)
954+
});
955+
});
956+
});
957+
958+
describe("useRouterMatch", () => {
959+
it("matches on direct routes", async () => {
960+
let match;
961+
962+
const Foo = () => {
963+
match = useRouterMatch("/foo");
964+
return ``;
965+
};
966+
967+
const { snapshot } = runWithNavigation(
968+
<Router>
969+
<Foo path="/foo" />
970+
</Router>,
971+
"/foo"
972+
);
973+
974+
expect(match).not.toBe(null);
975+
});
976+
977+
it("matches on matching child routes", () => {
978+
let matchExact;
979+
let matchSplat;
980+
981+
const Foo = () => {
982+
matchExact = useRouterMatch("/foo");
983+
matchSplat = useRouterMatch("/foo/*");
984+
return ``;
985+
};
986+
987+
const Bar = () => "";
988+
989+
const { snapshot } = runWithNavigation(
990+
<Router>
991+
<Foo path="/foo">
992+
<Bar path="/bar" />
993+
</Foo>
994+
</Router>,
995+
"/foo/bar"
996+
);
997+
998+
expect(matchExact).toBe(null);
999+
expect(matchSplat).not.toBe(null);
1000+
});
1001+
});
1002+
});

src/lib/history.test.js

-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ describe("navigate", () => {
3434

3535
it("should have a proper search", () => {
3636
const testHistory = createHistory(createMemorySource("/test"));
37-
console.log(testHistory);
3837
testHistory.navigate("/?asdf");
3938
expect(testHistory.location.search).toEqual("?asdf");
4039
});

website/src/Nav.js

+12
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,18 @@ let Nav = () => (
156156
<NavLink to="api/navigate">navigate</NavLink>
157157
</div>
158158

159+
<Header>Hooks API</Header>
160+
<div
161+
css={{
162+
fontFamily: `'SFMono-Regular', Consolas, 'Roboto Mono', 'Droid Sans Mono', 'Liberation Mono', Menlo, Courier, monospace`
163+
}}
164+
>
165+
<NavLink to="api/useLocation">useLocation</NavLink>
166+
<NavLink to="api/useMatch">useMatch</NavLink>
167+
<NavLink to="api/useNavigate">useNavigate</NavLink>
168+
<NavLink to="api/useParams">useParams</NavLink>
169+
</div>
170+
159171
<Header>Additional API</Header>
160172
<div
161173
css={{
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# useLocation
2+
3+
Returns the location to any component.
4+
5+
This API requires a hook-compatible version of React.
6+
7+
```jsx
8+
import { useLocation } from "@reach/router"
9+
10+
const useAnalytics = (props) => {
11+
const location = useLocation();
12+
13+
useEffect(() => {
14+
ga.send(['pageview', location.pathname]);
15+
}, [])
16+
)
17+
```

website/src/markdown/api/useMatch.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# useMatch
2+
3+
Matches a path to the location. Matching is relative to any parent Routers, but not parent match's, because they render even if they don't match.
4+
5+
This API requires a hook-compatible version of React.
6+
7+
```jsx
8+
import { useMatch } from "@reach/router"
9+
10+
const App = () => {
11+
const match = useMatch('/hot/:item');
12+
13+
return match ? (
14+
<div>Hot {match.item}</div>
15+
) : (
16+
<div>Uncool</div>
17+
)
18+
)
19+
```
20+
21+
`useMatch` will return `null` if your path does not match the location. If it does match it will contain:
22+
23+
- `uri`
24+
- `path`
25+
- `:params`
26+
27+
## match\[param\]: string
28+
29+
Any params in your the path will be parsed and passed as `match[param]` to your callback.
30+
31+
```jsx
32+
const match = useMatch("events/:eventId")
33+
34+
props.match ? props.match.eventId : "No match"
35+
```
36+
37+
## match.uri: string
38+
39+
The portion of the URI that matched. If you pass a wildcard path, the wildcard portion will not be included. Not sure how this is useful for a `Match`, but it's critical for how focus managment works, so we might as well pass it on to Match if we pass it on to Route Components!
40+
41+
```jsx
42+
// URL: /somewhere/deep/i/mean/really/deep
43+
const match = useMatch("/somewhere/deep/*")
44+
45+
return <div>{match.uri}</div>
46+
```
47+
48+
## match.path: string
49+
50+
The path you passed in as a prop.

0 commit comments

Comments
 (0)