├── website ├── .gitignore ├── src │ ├── markdown │ │ ├── .prettierrc │ │ ├── tutorial │ │ │ ├── 02-installation.md │ │ │ ├── 01-intro.md │ │ │ ├── 04-router.md │ │ │ ├── 10-next-steps.md │ │ │ ├── 03-link.md │ │ │ ├── 08-default-routes.md │ │ │ ├── 07-index-routes.md │ │ │ ├── 05-url-parameters.md │ │ │ ├── 09-navigate.md │ │ │ └── 06-nesting.md │ │ ├── api │ │ │ ├── useParams.md │ │ │ ├── useLocation.md │ │ │ ├── createMemorySource.md │ │ │ ├── createHistory.md │ │ │ ├── useNavigate.md │ │ │ ├── redirectTo.md │ │ │ ├── ServerLocation.md │ │ │ ├── Location.md │ │ │ ├── useMatch.md │ │ │ ├── isRedirect.md │ │ │ ├── LocationProvider.md │ │ │ ├── Redirect.md │ │ │ ├── Match.md │ │ │ ├── Router.md │ │ │ ├── Link.md │ │ │ ├── navigate.md │ │ │ └── RouteComponent.md │ │ └── pages │ │ │ ├── typescript.md │ │ │ ├── server-config.md │ │ │ ├── ranking.md │ │ │ ├── accessibility.md │ │ │ ├── credits.md │ │ │ ├── server-rendering.md │ │ │ ├── nesting.md │ │ │ ├── large-scale.md │ │ │ └── intro.md │ ├── index.js │ ├── theme.js │ ├── .babelrc │ ├── polyfills.js │ ├── index.html │ ├── App.js │ └── Nav.js ├── build │ └── markdown-loader.js ├── package.json └── webpack.config.js ├── src ├── .babelrc └── lib │ ├── __snapshots__ │ └── utils.test.js.snap │ ├── history.test.js │ ├── history.js │ ├── utils.test.js │ └── utils.js ├── logo-vertical.png ├── logo-horizontal.png ├── .prettierignore ├── examples ├── crud │ ├── public │ │ ├── favicon.ico │ │ ├── manifest.json │ │ └── index.html │ ├── src │ │ ├── index.js │ │ ├── withCache.js │ │ ├── index.css │ │ ├── utils.js │ │ ├── registerServiceWorker.js │ │ └── App.js │ ├── config │ │ ├── jest │ │ │ ├── fileTransform.js │ │ │ └── cssTransform.js │ │ ├── polyfills.js │ │ ├── paths.js │ │ ├── env.js │ │ ├── webpackDevServer.config.js │ │ ├── webpack.config.dev.js │ │ └── webpack.config.prod.js │ ├── .gitignore │ ├── scripts │ │ ├── test.js │ │ ├── start.js │ │ └── build.js │ └── package.json └── README.md ├── .gitignore ├── .eslintrc ├── .travis.yml ├── CONTRIBUTING.md ├── scripts └── test.js ├── .github └── ISSUE_TEMPLATE.md ├── rollup.config.js ├── README.md ├── LICENSE ├── CHANGELOG.md ├── package.json └── CODE_OF_CONDUCT.md /website/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules/ 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "../build/babel-preset" ] 3 | } 4 | -------------------------------------------------------------------------------- /logo-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reach/router/HEAD/logo-vertical.png -------------------------------------------------------------------------------- /logo-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reach/router/HEAD/logo-horizontal.png -------------------------------------------------------------------------------- /website/src/markdown/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 52, 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | /es 3 | /index.js 4 | /lib 5 | /umd 6 | website/dist 7 | 8 | -------------------------------------------------------------------------------- /examples/crud/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reach/router/HEAD/examples/crud/public/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /es 2 | /node_modules 3 | /umd 4 | /index.js 5 | /lib 6 | /compat 7 | npm-debug.log* 8 | yarn-error.log* 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "globals": { 4 | "__DEV__": true 5 | }, 6 | "rules": { 7 | "react/prop-types": ["warn"] 8 | } 9 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "8" 5 | - "10" 6 | 7 | script: 8 | - yarn lint 9 | - yarn test 10 | 11 | branches: 12 | only: 13 | - master 14 | cache: yarn 15 | -------------------------------------------------------------------------------- /website/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import App from "./App"; 4 | 5 | render(, document.getElementById("root")); 6 | 7 | //https://coolors.co/013d88-ffffff-3c91e6-9fd356-342e37 8 | -------------------------------------------------------------------------------- /examples/crud/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | const container = document.getElementById("root"); 7 | 8 | ReactDOM.render(, container); 9 | -------------------------------------------------------------------------------- /examples/crud/src/withCache.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SimpleCache } from "simple-cache-provider"; 3 | 4 | export default Comp => props => ( 5 | 6 | {cache => } 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /website/src/markdown/tutorial/02-installation.md: -------------------------------------------------------------------------------- 1 | # Tutorial - Installation 2 | 3 | If you're following along with code sandbox for this tutorial, Reach Router is already installed. 4 | 5 | In your own apps you can install it with npm or yarn. 6 | 7 | ```sh 8 | npm install @reach/router 9 | # or 10 | yarn add @reach/router 11 | ``` 12 | -------------------------------------------------------------------------------- /website/src/theme.js: -------------------------------------------------------------------------------- 1 | export const BLUE = "hsl(211, 81%, 36%)"; 2 | export const DARK_BLUE = "hsl(213, 99%, 15%)"; 3 | export const BLACK = "hsl(200, 8%, 15%)"; 4 | export const SMALL_BREAK_QUERY = "(max-width: 800px)"; 5 | export const SMALL_BREAK = `@media${SMALL_BREAK_QUERY}`; 6 | export const SIDEBAR_SIZE = 250; 7 | export const TOPBAR_SIZE = 30; 8 | -------------------------------------------------------------------------------- /examples/crud/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /website/src/markdown/tutorial/01-intro.md: -------------------------------------------------------------------------------- 1 | # Tutorial Introduction 2 | 3 | You can follow along with this tutorial with code sandbox right above this text. 4 | 5 | When you navigate from step to step, the sandbox will reload with code that's ready for the next step. You can also open the sandbox in a new window and even download it to run locally. Big thanks to Code Sandbox for such a great tool :D 6 | -------------------------------------------------------------------------------- /website/src/markdown/api/useParams.md: -------------------------------------------------------------------------------- 1 | # useParams 2 | 3 | Returns an object of the params for the route rendered. 4 | 5 | This API requires a hook-compatible version of React. 6 | 7 | ```jsx 8 | import { useParams } from "@reach/router" 9 | 10 | // route: /user/:userName 11 | const User = () => { 12 | const params = useParams(); 13 | 14 | return

{params.userName}

15 | ) 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/crud/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/crud/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return "module.exports = {};"; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return "cssTransform"; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /examples/crud/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /website/src/markdown/api/useLocation.md: -------------------------------------------------------------------------------- 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 | ``` 18 | -------------------------------------------------------------------------------- /website/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "syntax-dynamic-import", 4 | "transform-class-properties", 5 | "react-hot-loader/babel", 6 | ["transform-react-jsx", { "pragma": "Glamor.createElement" }] 7 | ], 8 | "presets": [ 9 | "@babel/preset-react", 10 | [ 11 | "@babel/preset-env", 12 | { 13 | "browsers": [">0.25%", "not ie 11", "not op_mini all"] 14 | } 15 | ] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /website/src/markdown/tutorial/04-router.md: -------------------------------------------------------------------------------- 1 | # Tutorial - Router 2 | 3 | Next we import [Router](../api/Router), render it, and render our [Route Components](../api/RouteComponent) as children. All that's left is to add a `path` prop to each child. 4 | 5 | ```jsx 6 | import { Link, Router } from "@reach/router" 7 | 8 | // under the `nav` 9 | 10 | 11 | 12 | 13 | ``` 14 | -------------------------------------------------------------------------------- /website/src/markdown/tutorial/10-next-steps.md: -------------------------------------------------------------------------------- 1 | # Tutorial - Next Steps 2 | 3 | Congratulations! You now know everything you need to know to get started with Reach Router. Above this text is a the full application we just built. 4 | 5 | So what's next? Go build something, of course. Then come back here to browse the examples, read the guides, and give the API references a skim. Shouldn't take more than 30 minutes to read it all. 6 | 7 | Let us know how it goes! 8 | -------------------------------------------------------------------------------- /website/src/markdown/tutorial/03-link.md: -------------------------------------------------------------------------------- 1 | # Tutorial - Link 2 | 3 | The first thing we want to do is add a Link. Here we import it, and then render a couple of them. Go ahead and click them and watch the URL change. This is the primary way users navigate around your app. 4 | 5 | ```jsx 6 | import { Link } from "@reach/router"; 7 | 8 | // ... 9 |

Tutorial!

10 | 14 | ``` 15 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Running Examples 2 | 3 | Each example uses create-react-app. To develop against the examples you just need to link @reach/router. 4 | 5 | ``` 6 | # from the root of the repository 7 | yarn link @reach/router 8 | cd examples/crud 9 | yarn && yarn start 10 | ``` 11 | 12 | There's a postinstall hook in each example that consumes the link. 13 | 14 | Next you'll want to open a new terminal tab and watch: 15 | 16 | ``` 17 | # from the root of the repository 18 | yarn watch 19 | ``` 20 | -------------------------------------------------------------------------------- /website/src/markdown/api/createMemorySource.md: -------------------------------------------------------------------------------- 1 | # createMemorySource(initialPath) 2 | 3 | Creates a source for [`createHistory`](createHistory) that manages a history stack in memory. Mostly for testing. 4 | 5 | ```jsx 6 | import { 7 | createMemorySource, 8 | createHistory 9 | } from "@reach/router" 10 | 11 | // for some types of tests you want a memory source 12 | let source = createMemorySource("/starting/url") 13 | let history = createHistory(source) 14 | ``` 15 | 16 | ## initialPath: string 17 | 18 | The initial path of the history. 19 | -------------------------------------------------------------------------------- /website/src/markdown/api/createHistory.md: -------------------------------------------------------------------------------- 1 | # createHistory(source) 2 | 3 | Creates a history object that enables you to listen for location changes. You don't typically need this outside of some types of testing. 4 | 5 | ```jsx 6 | import { 7 | createMemorySource, 8 | createHistory 9 | } from "@reach/router" 10 | 11 | // listen to the browser history 12 | let history = createHistory(window) 13 | 14 | // for some types of tests you want a memory source 15 | let source = createMemorySource("/starting/url") 16 | let history = createHistory(source) 17 | ``` 18 | -------------------------------------------------------------------------------- /website/src/markdown/api/useNavigate.md: -------------------------------------------------------------------------------- 1 | # useNavigate 2 | 3 | If you need to navigate programmatically (like after a form submits), this hook gives you an API to do so with a signature like this: 4 | 5 | ```js 6 | navigate(to, { state={}, replace=false }) 7 | ``` 8 | 9 | This API requires a hook-compatible version of React. 10 | 11 | ```jsx 12 | import { useNavigate } from "@reach/router" 13 | 14 | const AnalyticTracker = (props) => { 15 | const navigate = useNavigate(); 16 | 17 | return ( 18 |
navigate('../', { replace: true })}> 19 | {...} 20 |
21 | ) 22 | ) 23 | ``` 24 | -------------------------------------------------------------------------------- /website/src/polyfills.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | if (typeof Promise === "undefined") { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require("promise/lib/rejection-tracking").enable(); 8 | window.Promise = require("promise/lib/es6-extensions.js"); 9 | } 10 | 11 | // Object.assign() is commonly used with React. 12 | // It will use the native implementation if it's present and isn't buggy. 13 | Object.assign = require("object-assign"); 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Pull Requests 2 | 3 | Please don't send pull requests for new features, open an issue to discuss. 4 | 5 | ## Running tests 6 | 7 | This repo uses Jest. 8 | 9 | ```sh 10 | yarn test 11 | # or 12 | yarn test --watch 13 | ``` 14 | 15 | ## Developing the examples 16 | 17 | First you have to link the lib. 18 | 19 | ```sh 20 | # from the root 21 | yarn build 22 | yarn link 23 | ``` 24 | 25 | Then in one tab compile on file changes 26 | 27 | ``` 28 | yarn watch 29 | ``` 30 | 31 | And in another tab run the example 32 | 33 | ```sh 34 | # in a new tab 35 | cd examples/ 36 | yarn link "@reach/router" 37 | yarn start 38 | ``` 39 | -------------------------------------------------------------------------------- /website/src/markdown/tutorial/08-default-routes.md: -------------------------------------------------------------------------------- 1 | # Tutorial - Default Routes 2 | 3 | When users end up at a URL that doesn't match any of your routes we'd like to indicate that, just like a 404. We accomplish this with a Default Route. 4 | 5 | First create a new component to render: 6 | 7 | ```jsx 8 | const NotFound = () =>

Sorry, nothing here

9 | ``` 10 | 11 | Next, add it to `` with a `default` prop. This indicates to `Router` that it should be rendered if no match is found. 12 | 13 | ```jsx 14 | 15 | 16 | {/*...*/} 17 | 18 | ``` 19 | 20 | Now type some random junk into the URL, like "/flakjsd" and you'll see the `NotFound` component renders. 21 | -------------------------------------------------------------------------------- /website/src/markdown/api/redirectTo.md: -------------------------------------------------------------------------------- 1 | # redirectTo(uri) 2 | 3 | React 16+ only. For React < 16 use [`navigate`](navigate) or [Redirect](Redirect). 4 | 5 | Imperatively redirects to a new location by throwing a redirect request. 6 | 7 | ```jsx 8 | import { redirectTo } from "@reach/router" 9 | 10 | class User extends React.Component { 11 | componentDidMount() { 12 | fetchUser().then(user => { 13 | if (user.optedIntoNewUI) { 14 | redirectTo("/the/new/digs") 15 | } 16 | }) 17 | } 18 | 19 | // ... 20 | } 21 | ``` 22 | 23 | ## uri: string 24 | 25 | The uri to redirect to. Must be absolute, it does not support relative paths. 26 | 27 | ```jsx 28 | redirectTo("/somewhere/else") 29 | ``` 30 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = "test"; 5 | process.env.NODE_ENV = "test"; 6 | process.env.PUBLIC_URL = ""; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on("unhandledRejection", err => { 12 | throw err; 13 | }); 14 | 15 | const jest = require("jest"); 16 | const argv = process.argv.slice(2); 17 | 18 | // Watch unless on CI or in coverage mode 19 | if (!process.env.CI && argv.indexOf("--coverage") < 0) { 20 | argv.push("--watch"); 21 | } 22 | 23 | jest.run(argv); 24 | -------------------------------------------------------------------------------- /examples/crud/scripts/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = "test"; 5 | process.env.NODE_ENV = "test"; 6 | process.env.PUBLIC_URL = ""; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on("unhandledRejection", err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require("../config/env"); 17 | 18 | const jest = require("jest"); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf("--coverage") < 0) { 23 | argv.push("--watch"); 24 | } 25 | 26 | jest.run(argv); 27 | -------------------------------------------------------------------------------- /website/src/markdown/api/ServerLocation.md: -------------------------------------------------------------------------------- 1 | # ServerLocation 2 | 3 | When server rendering, you need to wrap your app in a `ServerLocation`. This enables your Routers, Links, etc. to match a location on the server where there is no history to listen to. 4 | 5 | ```jsx 6 | const App = () => ( 7 | 8 | 9 | 10 | 11 | ) 12 | 13 | const markup = renderToString( 14 | 15 | 16 | 17 | ) 18 | ``` 19 | 20 | Please see the [Server Rendering Guide](../server-rendering) for the complete story on server rendering. 21 | 22 | ## url: string 23 | 24 | The URL from the server. 25 | 26 | ```jsx 27 | createServer((req, res) => { 28 | let markup = renderToString( 29 | 30 | ) 31 | }).listen(PORT) 32 | ``` 33 | -------------------------------------------------------------------------------- /website/src/markdown/tutorial/07-index-routes.md: -------------------------------------------------------------------------------- 1 | # Tutorial - Index Routes 2 | 3 | Rather than show a blank page at `/invoices`, let's add an Index Route. Index Routes are just another child of a route, except their path is `/`. An Index Route will render when no other sibling matches the location. 4 | 5 | First create a new component: 6 | 7 | ```jsx 8 | const InvoicesIndex = () => ( 9 |
10 |

11 | Maybe put some pretty graphs here or 12 | something. 13 |

14 |
15 | ) 16 | ``` 17 | 18 | Next, add it as a child of `` with a path of `/`. 19 | 20 | ```jsx 21 | 22 | 23 | 24 | 25 | 26 | {/*...*/} 27 | 28 | ``` 29 | 30 | Now click on the "invoices" link. When you visit "/invoices" you'll see the new component. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Think you found a bug? 2 | 3 | Please do one of these things: 4 | 5 | 1. Don't open an issue, open a pull request with a failing test. 6 | 2. Boil down your issue on codesandbox (https://codesandbox.io/s/lplnp60wnm). 7 | 3. Don't be upset if your bug report is closed without comment. If you don't have the time to prove the bug is not a problem with your own code then we certainly don't either 😅. 8 | 9 | ## Have a question? 10 | 11 | Questions are welcome. They'll usually be answered with a link to some docs. 12 | 13 | Also please join our Spectrum community (https://spectrum.chat/reach)! 14 | 15 | ## Feature Request? 16 | 17 | We keep the API and feature set pretty limited on purpose so it's unlikely we'll want it implemented, but you're welcome to make a suggestion. 18 | 19 | Also feel free to ask about the feature in our Spectrum community (https://spectrum.chat/reach) 20 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import uglify from "rollup-plugin-uglify"; 3 | import replace from "rollup-plugin-replace"; 4 | import commonjs from "rollup-plugin-commonjs"; 5 | import resolve from "rollup-plugin-node-resolve"; 6 | 7 | const config = { 8 | input: "src/index.js", 9 | output: { 10 | name: "ReachRouter", 11 | globals: { 12 | react: "React", 13 | "react-dom": "ReactDOM" 14 | } 15 | }, 16 | external: ["react", "react-dom"], 17 | plugins: [ 18 | babel({ 19 | exclude: "node_modules/**" 20 | }), 21 | resolve(), 22 | commonjs({ 23 | include: /node_modules/ 24 | }), 25 | replace({ 26 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV) 27 | }) 28 | ] 29 | }; 30 | 31 | if (process.env.NODE_ENV === "production") { 32 | config.plugins.push(uglify()); 33 | } 34 | 35 | export default config; 36 | -------------------------------------------------------------------------------- /examples/crud/config/polyfills.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | if (typeof Promise === "undefined") { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require("promise/lib/rejection-tracking").enable(); 8 | window.Promise = require("promise/lib/es6-extensions.js"); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require("whatwg-fetch"); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require("object-assign"); 17 | 18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 19 | // We don't polyfill it in the browser--this is user's responsibility. 20 | if (process.env.NODE_ENV === "test") { 21 | require("raf").polyfill(global); 22 | } 23 | -------------------------------------------------------------------------------- /website/src/markdown/api/Location.md: -------------------------------------------------------------------------------- 1 | # Location 2 | 3 | Typically you only have access to the location in Route Components, `Location` provides the location anywhere in your app with a child render prop. 4 | 5 | ```jsx 6 | 7 | {props => { 8 | props.location 9 | props.navigate 10 | }} 11 | 12 | 13 | // usually folks use some destructuring 14 | 15 | {({ location })=> { 16 | // ... 17 | }} 18 | 19 | ``` 20 | 21 | The most common use case is using this to pass `location` to a `Router` for animations. 22 | 23 | ```jsx 24 | const FadeTransitionRouter = props => ( 25 | 26 | {({ location }) => ( 27 | 28 | 33 | 34 | {props.children} 35 | 36 | 37 | 38 | )} 39 | 40 | ) 41 | ``` 42 | -------------------------------------------------------------------------------- /website/src/markdown/tutorial/05-url-parameters.md: -------------------------------------------------------------------------------- 1 | # Tutorial - URL Parameters 2 | 3 | Let's create another screen called `Invoice`. It expects a prop called `invoiceId`. You can imagine it being rendered like ``. 4 | 5 | ```jsx 6 | const Invoice = props => ( 7 |
8 |

Invoice {props.invoiceId}

9 |
10 | ) 11 | ``` 12 | 13 | Next add it to the ``. Note the path. That `:invoiceId` is called a "URL Parameter". You'll see what it does next. 14 | 15 | ```jsx 16 | 17 | 18 | {/*...*/} 19 | 20 | ``` 21 | 22 | Finally, link to the new route somewhere. 23 | 24 | ```jsx 25 | Invoice 123 26 | Invoice ABC 27 | ``` 28 | 29 | The name of the url parameter (`:invoiceId`) becomes a prop by the same name on your route component (`props.invoiceId`). It gets parsed from the URL and passed to you. 30 | 31 | Besides using it to render, it's common to use that prop in `componentDidMount` to fetch some data. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Reach Router 4 | 5 |

6 | 7 |

8 | Next Generation Routing for React 9 |

10 | 11 |

12 | 13 | 14 | 15 |

16 | 17 | ## Documentation 18 | 19 | [Documentation Site](https://reach.tech/router) 20 | 21 | You can also find the docs in the [website directory](./website/src/markdown). 22 | 23 | ## Community 24 | 25 | [Join us on Spectrum](https://spectrum.chat/reach) 26 | 27 | ## Legal 28 | 29 | MIT License 30 | Copyright (c) 2018-present, Ryan Florence 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present, Ryan Florence 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /website/build/markdown-loader.js: -------------------------------------------------------------------------------- 1 | const markdownIt = require("markdown-it"); 2 | const Prism = require("prismjs"); 3 | const cheerio = require("cheerio"); 4 | 5 | let aliases = { 6 | js: "jsx", 7 | html: "markup", 8 | sh: "bash" 9 | }; 10 | 11 | let highlight = (str, lang) => { 12 | if (!lang) { 13 | return str; 14 | } else { 15 | lang = aliases[lang] || lang; 16 | require(`prismjs/components/prism-${lang}.js`); 17 | if (Prism.languages[lang]) { 18 | return Prism.highlight(str, Prism.languages[lang]); 19 | } else { 20 | return str; 21 | } 22 | } 23 | }; 24 | 25 | let md = markdownIt({ 26 | html: true, 27 | linkify: true, 28 | typographer: true, 29 | highlight 30 | }); 31 | 32 | let getTitle = html => 33 | cheerio 34 | .load(html)("h1") 35 | .text(); 36 | 37 | module.exports = markdown => { 38 | let html = md.render(markdown); 39 | return ` 40 | import React from 'react' 41 | export const title = ${JSON.stringify(getTitle(html))}; 42 | export default function() { 43 | return React.createElement( 44 | 'div', 45 | { 46 | className: 'markdown', 47 | dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)}} 48 | } 49 | ) 50 | } 51 | `; 52 | }; 53 | -------------------------------------------------------------------------------- /website/src/markdown/pages/typescript.md: -------------------------------------------------------------------------------- 1 | # TypeScript 2 | 3 | Reach Router may be used with [TypeScript](https://www.typescriptlang.org/). 4 | 5 | ## Install Typings 6 | 7 | ```sh 8 | npm install @types/reach__router --save-dev 9 | # or 10 | yarn add @types/reach__router --dev 11 | ``` 12 | 13 | ## Rendering 14 | 15 | To set props like `path` and `default` on routes, use the `RouteComponentProps` interface. 16 | 17 | ```tsx 18 | import * as React from "react" 19 | import { render } from "react-dom" 20 | import { Router, RouteComponentProps, Link } from "@reach/router" 21 | 22 | let Home = (props: RouteComponentProps) =>
Home
23 | let Dash = (props: RouteComponentProps) =>
Dash
24 | 25 | render( 26 | 27 | 28 | 29 | 30 | ) 31 | ``` 32 | 33 | ## Parse Data From the URL 34 | 35 | To access data parsed from the URL, create a new interface that extends `RouteComponentProps`, and add each of the URL's dynamic segments as a new prop. These props should be optional and typed as `string`. 36 | 37 | ```tsx 38 | // at url "/invoice/23" 39 | 40 | render( 41 | 42 | 43 | 44 | 45 | ) 46 | 47 | interface InvoiceProps extends RouteComponentProps 48 | { 49 | invoiceId?: string; 50 | } 51 | 52 | const Invoice = (props: InvoiceProps) => ( 53 |
54 |

Invoice {props.invoiceId}

55 |
56 | ) 57 | ``` 58 | -------------------------------------------------------------------------------- /website/src/markdown/pages/server-config.md: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | 3 | If your app works fine until you hit "reload" or manually type in a URL and then get a 404 error, then your server is not configured correctly. 4 | 5 | Whether you are server-rendering or not, all apps using Reach Router need to be configured to deliver the same JavaScript code at every URL. 6 | 7 | For non-server rendered apps, we recommend develping with [create react app](https://github.com/facebook/create-react-app), and for your production file server [we recommend `serve`](https://github.com/zeit/serve#readme). 8 | 9 | If you can't use either of these tools, you will need to learn how to configure your server to serve your `index.html` file at every url. 10 | 11 | Here's an example in express: 12 | 13 | ```js 14 | const path = require("path") 15 | const express = require("express") 16 | const app = new express() 17 | 18 | // requests for static files in the "public" directory 19 | // like JavaScript, CSS, images will be served 20 | app.use(express.static("public")) 21 | 22 | // Every other request will send the index.html file that 23 | // contains your application 24 | app.use("*", function(req, resp) { 25 | resp.sendFile("/public/index.html") 26 | }) 27 | 28 | app.listen("8000") 29 | ``` 30 | 31 | Paul Sherman has written an in-depth article about this, if you're still unclear we recommend you give it a read: [Single-Page Applications and the Server](https://blog.pshrmn.com/single-page-applications-and-the-server/) 32 | -------------------------------------------------------------------------------- /website/src/markdown/api/useMatch.md: -------------------------------------------------------------------------------- 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 |
Hot {match.item}
15 | ) : ( 16 |
Uncool
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
{match.uri}
46 | ``` 47 | 48 | ## match.path: string 49 | 50 | The path you passed in as a prop. 51 | -------------------------------------------------------------------------------- /website/src/markdown/api/isRedirect.md: -------------------------------------------------------------------------------- 1 | # isRedirect(error) 2 | 3 | Returns true if the error is a redirect request. Useful for server rendering and rethrowing errors in `componentDidCatch`. 4 | 5 | ## componentDidCatch 6 | 7 | If you're using `componentDidCatch` in your app you _must_ check if the error is a redirect, and if it is, rethrow it, otherwise the app will not redirect. Even better, you should check if the error is the kind you want to catch and rethrow if not. 8 | 9 | ```jsx 10 | import { isRedirect } from "@reach/router" 11 | 12 | class Decent extends React.Component { 13 | componentDidCatch(error) { 14 | if (isRedirect(error)) { 15 | throw error 16 | } else { 17 | // do whatever you were going to do 18 | } 19 | } 20 | } 21 | ``` 22 | 23 | Maybe one day we'll get pattern matching and a two-pass try/catch but those are just dreams in Sebastian Markbåge's head. 24 | 25 | ## Server Rendering 26 | 27 | If your app redirects while server rendering it will throw an error. Use `isRedirect` to decide how to handle the error. If it's a redirect, then redirect on your server, otherwise do what you normally do with errors. 28 | 29 | ```jsx 30 | let markup 31 | try { 32 | markup = renderToString( 33 | 34 | 35 | 36 | ) 37 | } catch (error) { 38 | if (isRedirect(error)) { 39 | res.redirect(error.uri) 40 | } else { 41 | // .. 42 | } 43 | } 44 | ``` 45 | 46 | Please see the [Server Rendering](../server-rendering) doc for the full server rendering story. 47 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "webpack", 8 | "start": "webpack-dev-server --open", 9 | "now-start": "serve dist --single" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.0.0-beta.46", 13 | "@babel/preset-env": "^7.0.0-beta.46", 14 | "babel-core": "^6.26.3", 15 | "babel-loader": "^8.0.0-beta", 16 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 17 | "babel-plugin-transform-class-properties": "^6.24.1", 18 | "babel-plugin-transform-react-jsx": "^6.24.1", 19 | "cheerio": "^1.0.0-rc.2", 20 | "clean-webpack-plugin": "^0.1.19", 21 | "prettier": "^1.12.1", 22 | "react-hot-loader": "^4.1.2", 23 | "webpack": "^4.6.0", 24 | "webpack-cli": "^2.1.2", 25 | "webpack-dev-server": "^3.1.4" 26 | }, 27 | "dependencies": { 28 | "@babel/preset-react": "^7.0.0-beta.46", 29 | "@reach/router": "latest", 30 | "@reactions/component": "^2.0.2", 31 | "glamor": "^2.20.40", 32 | "html-webpack-plugin": "^3.2.0", 33 | "markdown-it": "^8.4.1", 34 | "object-assign": "^4.1.1", 35 | "prismjs": "^1.21.0", 36 | "promise": "^8.0.1", 37 | "prop-types": "15", 38 | "react": "16.4", 39 | "react-dom": "16.4", 40 | "react-media": "^1.8.0", 41 | "scroll-into-view-if-needed": "^2.2.7", 42 | "serve": "^10.1.2", 43 | "uglifyjs-webpack-plugin": "^1.2.5", 44 | "unified": "^6.2.0", 45 | "unist-util-visit": "^1.3.1" 46 | }, 47 | "prettier": {} 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/__snapshots__/utils.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`match works 1`] = ` 4 | Object { 5 | "params": Object { 6 | "bar": "hello", 7 | }, 8 | "route": Object { 9 | "path": "/foo/:bar", 10 | }, 11 | "uri": "/foo/hello", 12 | } 13 | `; 14 | 15 | exports[`pick dynamic segments + splat return value 1`] = ` 16 | Object { 17 | "params": Object { 18 | "*": "some/deep/path", 19 | "userId": "ryan", 20 | }, 21 | "route": Object { 22 | "path": "/users/:userId/files/*", 23 | }, 24 | "uri": "/users/ryan/files", 25 | } 26 | `; 27 | 28 | exports[`pick pick /* 1`] = ` 29 | Object { 30 | "params": Object { 31 | "*": "whatever/else", 32 | }, 33 | "route": Object { 34 | "path": "/*", 35 | }, 36 | "uri": "/", 37 | } 38 | `; 39 | 40 | exports[`pick pick return value 1`] = ` 41 | Object { 42 | "params": Object { 43 | "five": "five", 44 | "four": "four", 45 | "one": "one", 46 | "three": "three", 47 | "two": "two", 48 | }, 49 | "route": Object { 50 | "path": "/:one/:two/:three/:four/:five", 51 | "value": "Fiver", 52 | }, 53 | "uri": "/one/two/three/four/five", 54 | } 55 | `; 56 | 57 | exports[`pick query strings 1`] = ` 58 | Object { 59 | "params": Object { 60 | "userId": "ryan", 61 | }, 62 | "route": Object { 63 | "path": "/users/:userId", 64 | }, 65 | "uri": "/users/ryan", 66 | } 67 | `; 68 | 69 | exports[`pick splat return value 1`] = ` 70 | Object { 71 | "params": Object { 72 | "*": "some/deep/path", 73 | }, 74 | "route": Object { 75 | "path": "/files/*", 76 | "value": "FilesDeep", 77 | }, 78 | "uri": "/files", 79 | } 80 | `; 81 | -------------------------------------------------------------------------------- /website/src/markdown/pages/ranking.md: -------------------------------------------------------------------------------- 1 | # Path Ranking 2 | 3 | Reach router ranks your paths so you don't have to worry about the order of your paths or pass in props to help it know how to match. Generally, you shouldn't have to worry about this at all, just don't think about it and it will do exactly what you want it to. 4 | 5 | However, I know you're a programmer and so you want to know how it works. 6 | 7 | A path is assigned a score based on the value of each segment in the path. The path with the highest score that matches the location wins. 8 | 9 | * Each segment gets 4 points and then... 10 | * Static segments get 3 more points 11 | * Dynamic segments 2 more 12 | * Root segments 1 13 | * and finally wildcard segments get a 1 point penalty 14 | 15 | Here's a "table" showing different paths and their scores: 16 | 17 | | Score | Path | 18 | | ----- | -------------------------------- | 19 | | 5 | `/` | 20 | | 7 | `/groups` | 21 | | 13 | `/groups/:groupId` | 22 | | 14 | `/groups/mine` | 23 | | 19 | `/groups/:groupId/users/\*` | 24 | | 20 | `/groups/:groupId/users` | 25 | | 21 | `/groups/mine/users` | 26 | | 24 | `/:one/:two/:three/:four` | 27 | | 26 | `/groups/:groupId/users/:userId` | 28 | | 27 | `/groups/:groupId/users/me` | 29 | | 28 | `/groups/mine/users/me` | 30 | | 30 | `/:one/:two/:three/:four/:five` | 31 | 32 | A URL like "/groups/mine" matches both paths "/groups/:groupId" and "/groups/mine", but "/groups/mine" has 14 points while the other only has 13, so "/groups/mine" wins. 33 | -------------------------------------------------------------------------------- /examples/crud/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | margin: 0; 4 | } 5 | 6 | .App { 7 | height: 100vh; 8 | display: flex; 9 | } 10 | 11 | .primary-nav { 12 | width: 200px; 13 | border-right: solid 1px #ccc; 14 | overflow: auto; 15 | position: relative; 16 | } 17 | 18 | .nav-link { 19 | display: block; 20 | padding: 10px 15px; 21 | font-size: 120%; 22 | text-decoration: none; 23 | border-bottom: solid 1px #eee; 24 | color: rgb(0, 0, 238); 25 | } 26 | 27 | .nav-link.active, 28 | .nav-link:active { 29 | color: red; 30 | } 31 | 32 | .main-content { 33 | padding: 0px 20px; 34 | } 35 | 36 | /* heck yes I copy pasted this from stack overflow */ 37 | /* https://stackoverflow.com/questions/1367409/how-to-make-button-look-like-a-link */ 38 | .text-button { 39 | align-items: normal; 40 | background-color: rgba(0, 0, 0, 0); 41 | border-color: rgb(0, 0, 238); 42 | border-style: none; 43 | box-sizing: content-box; 44 | color: rgb(0, 0, 238); 45 | cursor: pointer; 46 | display: inline; 47 | font: inherit; 48 | height: auto; 49 | padding: 0; 50 | perspective-origin: 0 0; 51 | text-align: start; 52 | text-decoration: underline; 53 | transform-origin: 0 0; 54 | width: auto; 55 | -moz-appearance: none; 56 | -webkit-logical-height: 1em; 57 | -webkit-logical-width: auto; 58 | } 59 | 60 | @supports (-moz-appearance: none) { 61 | button::-moz-focus-inner { 62 | border: none; 63 | padding: 0; 64 | } 65 | button:focus { 66 | outline-style: dotted; 67 | outline-width: 1px; 68 | } 69 | } 70 | 71 | .edit { 72 | color: rgb(0, 0, 238); 73 | } 74 | 75 | .Field { 76 | display: block; 77 | margin: 1.2em 0; 78 | } 79 | -------------------------------------------------------------------------------- /website/src/markdown/tutorial/09-navigate.md: -------------------------------------------------------------------------------- 1 | # Tutorial - Navigating Imperatively 2 | 3 | Sometimes you need to navigate in response to something other than the user clicking on a link. For this we have `navigate`. Let's import `navigate`. 4 | 5 | ```jsx 6 | import { 7 | Router, 8 | Link, 9 | navigate 10 | } from "@reach/router" 11 | ``` 12 | 13 | Probably the most common reason to use `navigate` is a form submission. Perhaps the user submits a form, you save some data, and then navigate to the record. Let's add this form to `Invoices`: 14 | 15 | ```jsx 16 | const Invoices = props => ( 17 |
18 | {/* ... */} 19 | 20 |
{ 22 | event.preventDefault() 23 | const id = event.target.elements[0].value 24 | event.target.reset() 25 | 26 | // pretend like we saved a record to the DB here 27 | // and then we navigate imperatively 28 | navigate(`/invoices/${id}`) 29 | }} 30 | > 31 |

32 | 35 | 36 |

37 |
38 | 39 | {props.children} 40 |
41 | ) 42 | ``` 43 | 44 | Go ahead and submit the form and watch the router navigate to the new invoice. 45 | 46 | Oh, one more thing. Route Components get a `navigate` prop. This version of the function is aware of its position in the hierarchy. This means you can navigate to relative paths the same way you can link to them. 47 | 48 | Go ahead and remove the spot where we imported navigate and let's use the prop instead: 49 | 50 | ```jsx 51 | navigate(`/invoices/${id}`) 52 | // becomes 53 | props.navigate(id) 54 | ``` 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.3.4 2 | 3 | - d7e4cfc Support forwarding url params with wildcard paths 4 | 5 | ## v1.3.3 6 | 7 | _1.3.2 was a failed publish_ 8 | 9 | - 8b93694 Fix an encoding issue with browsers who do not encode urls 10 | - d0962d7 Fix a bug on specific vendors that do not ship `location.pathname` 11 | - 244e2bf Improve compatibility for `createMemorySource` 12 | 13 | ## v1.3.1 14 | 15 | - f40ea53 Fix an accidental breaking change not exposing all location properties 16 | 17 | ## v1.3.0 18 | 19 | - 56d4dca Added 4 new Hook APIs! 20 | - `useLocation` 21 | - `useParams` 22 | - `useNavigate` 23 | - `useMatch` 24 | - 15298df Improved displayName for react context 25 | - 11e9ed6 Fixed a bug that pushed to history when a user clicks the same path 26 | - ccfc3c8 Added support for trailing wildcard names e.g., `path="/files/*filePath"` 27 | - ad52cd3 Upgraded create-react-context to an MIT license compatible version 28 | - 0a8af93 Fixed a bug with checking own property on locations 29 | - 28a79e7 Fixed `search` not being prepended with a `?` 30 | - 77fa233 Added displayName to Link 31 | 32 | ## v1.2.1 33 | 34 | - 1f9f908 replace unstable_deferredUpdates with rAF 35 | 36 | ## v1.2.0 37 | 38 | - a7a1c84 fixed focus being stolen from [autofocus] elements 39 | - 8a56262 Added `hash` and `search` to server location 40 | - 1c13f8a Added history "PUSH" and "POP" actions to listener callback 41 | - 534f67c Added globalHistory to index exports 42 | - 8cb6e83 allow falsey children in Router 43 | 44 | ## v1.1.1 45 | 46 | - e0338b5c Removed vestigial dependencies from package.json 47 | - fix/automated linting 48 | 49 | ## v1.1.0 50 | 51 | - 53f06958 Added ref forwarding and `innerRef` fallback 52 | 53 | ## v1.0.0 54 | 55 | - Added literally everything. 56 | -------------------------------------------------------------------------------- /examples/crud/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/crud/src/utils.js: -------------------------------------------------------------------------------- 1 | const sleep = (ms = 1000) => new Promise(res => setTimeout(res, ms)); 2 | 3 | const API = `https://contacts.now.sh`; 4 | // const API = `http://localhost:5000`; 5 | 6 | let token = null; 7 | 8 | export const login = () => 9 | new Promise(async (res, rej) => { 10 | token = localStorage.getItem("token"); 11 | if (token) { 12 | res(token); 13 | } else { 14 | token = prompt("Give me a token, anything will do!"); 15 | if (token.trim() === "") 16 | token = Math.random() 17 | .toString(32) 18 | .substr(2, 8); 19 | localStorage.setItem("token", token); 20 | // fake some async waiting 21 | await sleep(); 22 | res(token); 23 | } 24 | }); 25 | 26 | const fetchContacts = async (url, opts = { headers: {} }) => { 27 | return fetch(`${API}${url}`, { 28 | ...opts, 29 | headers: { authorization: token, ...opts.headers } 30 | }).then(res => { 31 | if (res.ok) { 32 | return res.json(); 33 | } else { 34 | return res; 35 | } 36 | }); 37 | }; 38 | 39 | export const getContacts = () => fetchContacts("/contacts"); 40 | 41 | export const getContact = id => fetchContacts(`/contacts/${id}`); 42 | 43 | export const updateContact = (id, contact) => 44 | fetchContacts(`/contacts/${id}`, { 45 | method: "put", 46 | headers: { "Content-Type": "application/json" }, 47 | body: JSON.stringify({ contact }) 48 | }); 49 | 50 | export const deleteContact = id => 51 | fetchContacts(`/contacts/${id}`, { 52 | method: "delete" 53 | }); 54 | 55 | export const createContact = contact => 56 | fetchContacts("/contacts", { 57 | method: "post", 58 | headers: { "Content-Type": "application/json" }, 59 | body: JSON.stringify({ contact }) 60 | }); 61 | -------------------------------------------------------------------------------- /website/src/markdown/api/LocationProvider.md: -------------------------------------------------------------------------------- 1 | # LocationProvider 2 | 3 | Sets up a listener to location changes. The primary API's that need the location automatically set this up for you. This is mostly useful for testing, and if we ever decide to get into React Native, it'll be useful there too. 4 | 5 | ```jsx 6 | import { 7 | createMemorySource, 8 | createHistory, 9 | LocationProvider 10 | } from "@reach/router" 11 | 12 | let history = createHistory(window) 13 | 14 | // for some types of tests you want a memory source 15 | let source = createMemorySource("/starting/url") 16 | let history = createHistory(source) 17 | 18 | let App = () => ( 19 | 20 |
21 | Alright, we've established some location 22 | context 23 |
24 |
25 | ) 26 | ``` 27 | 28 | ## history: object (optional) 29 | 30 | The history to listen to. Defaults to the browser history or a memory history if a DOM is not found. 31 | 32 | ```jsx 33 | import { createHistory, LocationProvider } from '@reach/router' 34 | let history = createHistory(window) 35 | 36 | 37 | ``` 38 | 39 | ## children: element 40 | 41 | You can pass elements as children to wrap an app in location context. 42 | 43 | ```jsx 44 | 45 |
This is fine
46 |
47 | ``` 48 | 49 | ## children: func 50 | 51 | If you pass a child render function `LocationProvider` will pass you the context it creates: `location` and `navigate`. If you want access to these values somewhere arbitrary in your app, use [Location](Location) instead. 52 | 53 | ```jsx 54 | 55 | {context => { 56 | console.log(context.location) 57 | console.log(context.navigate) 58 | }} 59 | 60 | ``` 61 | -------------------------------------------------------------------------------- /website/src/markdown/pages/accessibility.md: -------------------------------------------------------------------------------- 1 | # Accessibility 2 | 3 | Accessibility is a first-class concern for Reach Router, so, naturally, it's baked in. 4 | 5 | ## Links 6 | 7 | Links in a client rendered app should behave just like links in a server rendered app. Reach Router does this automatically for you: 8 | 9 | - Keyboard access 10 | - Assistive devices announce correctly 11 | - Command/Control click opens in a new tab 12 | - Right click "open in new window" opens in a new window 13 | 14 | ## Focus Management 15 | 16 | Whenever the content of a page changes in response to a user interaction, the focus should be moved to that content; otherwise, users on assistive devices have to search around the page to find what changed--yuck! Without the help of a router, managing focus on route transitions requires a lot effort and knowledge on your part. 17 | 18 | Reach Router provides out-of-the-box focus management so your apps are significantly more accessible without you breaking a sweat. 19 | 20 | When the location changes, the top-most part of your application that changed is identified and focus is moved to it. 21 | 22 | ## Focus Management 23 | 24 | Prior to version `1.3`, we used `role="group"` on the top-level element so that screen readers would announce to the user the focused group's nested elements similarly to how it works when a user loads a page for the first time. A problem we found is that some screen readers (notably VoiceOver and NVDA with Firefox) will read the group's content as if it's a long string, void of any important context provided by the interior markup. 25 | 26 | While we removed the group role as the router's default setting, if you believe the group role creates a better experience for your application you can still pass it as a prop to the `Router` component and it will be forwarded to the top-level element and function the same as before. 27 | -------------------------------------------------------------------------------- /website/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const UglifyJSPlugin = require("uglifyjs-webpack-plugin"); 3 | const path = require("path"); 4 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 5 | const CleanWebpackPlugin = require("clean-webpack-plugin"); 6 | 7 | const PROD = process.env.NODE_ENV === "production"; 8 | 9 | module.exports = { 10 | entry: { 11 | app: [require.resolve("./src/polyfills"), "./src/index.js"], 12 | vendor: ["react", "react-dom"] 13 | }, 14 | mode: PROD ? "production" : "development", 15 | devtool: PROD ? undefined : "inline-source-map", 16 | devServer: { 17 | contentBase: "./dist", 18 | hot: true, 19 | historyApiFallback: true 20 | }, 21 | output: { 22 | filename: "static/js/[name].[hash:8].js", 23 | chunkFilename: "static/js/[name].[chunkhash:8].chunk.js", 24 | path: path.resolve(__dirname, "dist"), 25 | publicPath: PROD ? "/router/" : "/" 26 | }, 27 | optimization: { 28 | splitChunks: { 29 | chunks: "all" 30 | } 31 | }, 32 | plugins: [ 33 | new CleanWebpackPlugin(["dist"]), 34 | new HtmlWebpackPlugin({ 35 | title: "Reach Router: Next Generation Routing for React", 36 | template: "src/index.html" 37 | }), 38 | new webpack.NamedModulesPlugin(), 39 | new webpack.DefinePlugin({ 40 | BASEPATH: JSON.stringify(PROD ? "/router" : "/"), 41 | VERSION: JSON.stringify(require("../package.json").version) 42 | }), 43 | new webpack.ProvidePlugin({ 44 | Glamor: "glamor/react" 45 | }) 46 | ].concat( 47 | PROD ? [new UglifyJSPlugin()] : [new webpack.HotModuleReplacementPlugin()] 48 | ), 49 | module: { 50 | rules: [ 51 | { 52 | test: /\.js$/, 53 | exclude: /(node_modules)/, 54 | use: "babel-loader" 55 | }, 56 | { 57 | test: /\.md$/, 58 | use: path.resolve("build/markdown-loader.js") 59 | } 60 | ] 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /website/src/markdown/pages/credits.md: -------------------------------------------------------------------------------- 1 | # Credits and Trade-offs 2 | 3 | Hey, it's Ryan Florence here. 4 | 5 | ## Credits 6 | 7 | ### React Router 8 | 9 | In May of 2014 I created the first version of React Router and was involved with the project all the way to the end of 2017 at version 4. Because of that, Reach Router is clearly influenced by React Router and all of the ideas of the contributors who built it. 10 | 11 | To me, Reach Router is everything I missed about v3 and everything I love about v4, plus a few things I've always wanted a router in React to have, particularly focus management and relative links. I want a more accessible web, especially in React. 12 | 13 | ### Ember + Preact Router 14 | 15 | When multiple routes match the location, Ember and Preact Router pick the route that you intend to match, rather than relying on order or props like React Router v4's `exact`. The path ranking feature of Reach Router was inspired by them both. 16 | 17 | ## Trade-offs (mostly compared to React Router) 18 | 19 | * Size. I'm aiming to come in under 4kb for a modern React app (where API polyfills aren't needed). That makes some extra features harder to include. 20 | 21 | * No complex route patterns. There are no optional params, or anything like them, it's just static paths, params, and trailing wildcard. 22 | 23 | * No history blocking. I found that the only use-case I had was preventing the user from navigating away from a half-filled out form. Not only is it pretty easy to just save the form state to session storage and bring it back when they return, but history blocking doesn't happen when you navigate away from the app (say to another domain). This kept me from actually using history blocking and always opting to save the form state to session storage. 24 | 25 | * No React Native support. Not yet anyway. An important goal is "accessible by default". Assuming a DOM makes this goal easier to attain and maintain. Also, there's a sea of navigation projects for React Native that I'm not interested in swimming in! Maybe in the future, though. 26 | -------------------------------------------------------------------------------- /website/src/markdown/api/Redirect.md: -------------------------------------------------------------------------------- 1 | # Redirect 2 | 3 | Redirects from one path to another. Use this when you want to change a URL without breaking links to the old path. 4 | 5 | ```jsx 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | ``` 17 | 18 | It doesn't have to be a child of a Router, it can also be rendered anywhere in your app when you want to declaratively redirect. 19 | 20 | ```jsx 21 | class Home extends React.Component { 22 | state = { 23 | user: null 24 | } 25 | 26 | async componentDidMount() { 27 | let user = await fetchUser() 28 | this.setState({ user }) 29 | } 30 | 31 | render() { 32 | if (this.state.user.hasBetaEnabled) { 33 | return 34 | } else { 35 | return 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | ## from: string 42 | 43 | Only used when rendered inside of a ``. This indicates which path to redirect from, note the parameters must match the `to` prop's parameters. 44 | 45 | ```jsx 46 | 50 | ``` 51 | 52 | ## to: string 53 | 54 | This indicates which path to redirect to, note the parameters must match the `from` prop's parameters. 55 | 56 | ```jsx 57 | 61 | ``` 62 | 63 | ## noThrow 64 | 65 | ```jsx 66 | 67 | ``` 68 | 69 | Redirect works with `componentDidCatch` to prevent the tree from rendering and starts over with a new location. 70 | 71 | Because React doesn't swallow the error this might bother you. For example, a redirect will trigger Create React App's error overlay. In production, everything is fine. If it bothers you, add `noThrow` and Redirect will do redirect without using `componentDidCatch`. 72 | 73 | If you're using React < 16 Redirect will not throw at all, regardless of what value you put for this prop. 74 | 75 | If you're using `componentDidCatch` in your app please read the [isRedirect](isRedirect) doc! 76 | -------------------------------------------------------------------------------- /examples/crud/config/paths.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const fs = require("fs"); 5 | const url = require("url"); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith("/"); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right