diff --git a/.grit/patterns/ReactRouterV5.md b/.grit/patterns/ReactRouterV5.md new file mode 100644 index 00000000..db5a3c22 --- /dev/null +++ b/.grit/patterns/ReactRouterV5.md @@ -0,0 +1,917 @@ +--- +title: Upgrading from v5 +order: 2 +--- + +# Upgradee React Router v5 to v6 + +```grit +language js + +pattern handle_redirect_switch() { + ReactNode(name=`Redirect`, props=$props) as $redirect => ` } />` where { + $props <: contains { `from="$from"` => . } + } +} + +pattern rename_switch() { + ReactNode(name=`Switch` => `Routes`, children=$children) where { + $children <: maybe contains handle_redirect_switch() + } +} + +rename_switch() +``` + + + +### Remove ``s inside `` + +Remove any `` elements that are directly inside a ``. + +If you want to redirect on the initial render, you should move the redirect logic to your server (we [wrote more about this here](https://gist.github.com/mjackson/b5748add2795ce7448a366ae8f8ae3bb)). + +If you want to redirect client-side, move your `` into a `` prop. + +```tsx + + + +``` + +```tsx + + } /> + +``` + +Normal `` elements that are not inside a `` are ok to remain. They will become `` elements in v6. + +### Refactor custom ``s + +Replace any elements inside a `` that are not plain `` elements with a regular ``. This includes any ``-style custom components. + +You can [read more about the rationale behind this here](https://gist.github.com/mjackson/d54b40a094277b7afdd6b81f51a0393f), including some tips about how to use a `` prop in v5 to achieve the same effect. + +### Ship it! + +Again, **once your app is upgraded to v5.1 you should test and deploy it**, and pick this guide back up when you're ready to continue. + +## Upgrade to React Router v6 + +**Heads up:** This is the biggest step in the migration and will probably take the most time and effort. + +For this step, you'll need to install React Router v6. If you're managing dependencies via npm: + +```bash +$ npm install react-router-dom +# or, for a React Native app +$ npm install react-router-native +``` + +You'll also want to remove the `history` dependency from your package.json. The `history` library is a direct dependency of v6 (not a peer dep), so you won't ever import or use it directly. Instead, you'll use the `useNavigate()` hook for all navigation (see below). + +### Upgrade all `` elements to `` + +React Router v6 introduces a `Routes` component that is kind of like `Switch`, but a lot more powerful. The main advantages of `Routes` over `Switch` are: + +- All ``s and ``s inside a `` are relative. This leads to + leaner and more predictable code in `` and `` +- Routes are chosen based on the best match instead of being traversed in order. + This avoids bugs due to unreachable routes because they were defined later + in your `` +- Routes may be nested in one place instead of being spread out in different + components. In small to medium-sized apps, this lets you easily see all your + routes at once. In large apps, you can still nest routes in bundles that you + load dynamically via `React.lazy` + +In order to use v6, you'll need to convert all your `` elements to ``. If you already made the upgrade to v5.1, you're halfway there. + +First, let's talk about relative routes and links in v6. + +### Relative Routes and Links + +In v5, you had to be very explicit about how you wanted to nest your routes and links. In both cases, if you wanted nested routes and links you had to build the `` and `` props from the parent route's `match.url` and `match.path` properties. Additionally, if you wanted to nest routes, you had to put them in the child route's component. + +```js +// This is a React Router v5 app +import { BrowserRouter, Switch, Route, Link, useRouteMatch } from 'react-router-dom'; + +function App() { + return ( + + + + + + + + + + + ); +} + +function Users() { + // In v5, nested routes are rendered by the child component, so + // you have elements all over your app for nested UI. + // You build nested routes and links using match.url and match.path. + let match = useRouteMatch(); + + return ( +
+ + + + + + + + + + +
+ ); +} +``` + +This is the same app in v6: + +```js +// This is a React Router v6 app +import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; + +function App() { + return ( + + + } /> + } /> + + + ); +} + +function Users() { + return ( +
+ + + + } /> + } /> + +
+ ); +} +``` + +A few important things to notice about v6 in this example: + +- `` and `` are relative. This means that they + automatically build on the parent route's path and URL so you don't have to + manually interpolate `match.url` or `match.path` +- `` is gone. Instead, routes with descendant routes (defined in + other components) use a trailing `*` in their path to indicate they match + deeply +- You may put your routes in whatever order you wish and the router will + automatically detect the best route for the current URL. This prevents bugs + due to manually putting routes in the wrong order in a `` + +You may have also noticed that all `` from the v5 app changed to `` in v6. Assuming you followed the upgrade steps to v5.1, this should be as simple as moving your route element from the child position to a named `element` prop. + + + +### Advantages of `` + +In the section about upgrading to v5.1, we promised that we'd discuss the advantages of using regular elements instead of components (or element types) for rendering. Let's take a quick break from upgrading and talk about that now. + +For starters, we see React itself taking the lead here with the `}>` API. The `fallback` prop takes a React element, not a component. This lets you easily pass whatever props you want to your `` from the component that renders it. + +Using elements instead of components means we don't have to provide a `passProps`-style API so you can get the props you need to your elements. For example, in a component-based API there is no good way to pass props to the `` element that is rendered when `` matches. Most React libraries who take this approach end up with either an API like `` or use a render prop or higher-order component. + +Also, in case you didn't notice, in v4 and v5 `Route`'s rendering API became rather large. It went something like this: + +```js +// Ah, this is nice and simple! + + +// But wait, how do I pass custom props to the element?? +// Hmm, maybe we can use a render prop in those situations? + ( + + )} +/> + +// Ok, now we have two ways to render something with a route. :/ + +// But wait, what if we want to render something when a route +// *doesn't* match the URL, like a Not Found page? Maybe we +// can use another render prop with slightly different semantics? + ( + match ? ( + + ) : ( + + ) + )} +/> + +// What if I want to get access to the route match, or I need +// to redirect deeper in the tree? +function DeepComponent(routeStuff) { + // got routeStuff, phew! +} +export default withRouter(DeepComponent); + +// Well hey, now at least we've covered all our use cases! +// ... *facepalm* +``` + +At least part of the reason for this API sprawl was that React did not provide any way for us to get the information from the `` to your route element, so we had to invent clever ways to get both the route data **and** your own custom props through to your elements: `component`, render props, `passProps` higher-order-components ... until **hooks** came along! + +Now, the conversation above goes like this: + +```js +// Ah, nice and simple API. And it's just like the API! +// Nothing more to learn here. +} /> + +// But wait, how do I pass custom props to the +// element? Oh ya, it's just an element. Easy. +} /> + +// Ok, but how do I access the router's data, like the URL params +// or the current location? +function Profile({ animate }) { + let params = useParams(); + let location = useLocation(); +} + +// But what about components deep in the tree? +function DeepComponent() { + // oh right, same as anywhere else + let navigate = useNavigate(); +} + +// Aaaaaaaaand we're done here. +``` + +Another important reason for using the `element` prop in v6 is that `` is reserved for nesting routes. This is one of people's favorite features from v3 and `@reach/router`, and we're bringing it back in v6. Taking the code in the previous example one step further, we can hoist all `` elements into a single route config: + +```js +// This is a React Router v6 app +import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom'; + +function App() { + return ( + + + } /> + }> + } /> + } /> + + + + ); +} + +function Users() { + return ( +
+ + + +
+ ); +} +``` + +This step is optional of course, but it's really nice for small to medium sized apps that don't have thousands of routes. + +Notice how `` elements nest naturally inside a `` element. Nested routes build their path by adding to the parent route's path. We didn't need a trailing `*` on `` this time because when the routes are defined in one spot the router is able to see all your nested routes. + +You'll only need the trailing `*` when there is another `` somewhere in that route's descendant tree. In that case, the descendant `` will match on the portion of the pathname that remains (see the previous example for what this looks like in practice). + +When using a nested config, routes with `children` should render an `` in order to render their child routes. This makes it easy to render layouts with nested UI. + +### Note on `` patterns + +React Router v6 uses a simplified path format. `` in v6 supports only 2 kinds of placeholders: dynamic `:id`-style params and `*` wildcards. A `*` wildcard may be used only at the end of a path, not in the middle. + +All of the following are valid route paths in v6: + +``` +/groups +/groups/admin +/users/:id +/users/:id/messages +/files/* +/files/:id/* +``` + +The following RegExp-style route paths are **not valid** in v6: + +``` +/users/:id? +/tweets/:id(\d+) +/files/*/cat.jpg +/files-* +``` + +We added the dependency on path-to-regexp in v4 to enable more advanced pattern matching. In v6 we are using a simpler syntax that allows us to predictably parse the path for ranking purposes. It also means we can stop depending on path-to-regexp, which is nice for bundle size. + +If you were using any of path-to-regexp's more advanced syntax, you'll have to remove it and simplify your route paths. If you were using the RegExp syntax to do URL param validation (e.g. to ensure an id is all numeric characters) please know that we plan to add some more advanced param validation in v6 at some point. For now, you'll need to move that logic to the component the route renders, and let it branch its rendered tree after you parse the params. + +If you were using `` you should move it to its containing `` prop. Either all routes in a `` element are case-sensitive or they are not. + +One other thing to notice is that all path matching in v6 ignores the trailing slash on the URL. In fact, `` has been removed and has no effect in v6. **This does not mean that you can't use trailing slashes if you need to.** Your app can decide to use trailing slashes or not, you just can't render two different UIs _client-side_ at `` and ``. You can still render two different UIs at those URLs (though we wouldn't recommend it), but you'll have to do it server-side. + +### Note on `` values + +In v5, a `` value that does not begin with `/` was ambiguous; it depends on what the current URL is. For example, if the current URL is `/users`, a v5 `` would render a ``. However, if the current URL has a trailing slash, like `/users/`, the same `` would render ``. This makes it difficult to predict how links will behave, so in v5 we recommended that you build links from the root URL (using `match.url`) and not use relative `` values. + +React Router v6 fixes this ambiguity. In v6, a `` will always render the same ``, regardless of the current URL. + +For example, a `` that is rendered inside a `` will always render a link to `/users/me`, regardless of whether or not the current URL has a trailing slash. + +When you'd like to link back "up" to parent routes, use a leading `..` segment in your `` value, similar to what you'd do in a ``. + +```tsx +function App() { + return ( + + }> + } /> + + + ); +} + +function Users() { + return ( +
+

+ {/* This links to /users - the current route */} + Users +

+ +
    + {users.map((user) => ( +
  • + {/* This links to /users/:id - the child route */} + {user.name} +
  • + ))} +
+
+ ); +} + +function UserProfile() { + return ( +
+

+ {/* This links to /users - the parent route */} + All Users +

+ +

+ {/* This links to /users/:id - the current route */} + User Profile +

+ +

+ {/* This links to /users/mj - a "sibling" route */} + MJ +

+
+ ); +} +``` + +It may help to think about the current URL as if it were a directory path on the filesystem and `` like the `cd` command line utility. + +``` +// If your routes look like this + + + + + + +// and the current URL is /app/dashboard (with or without +// a trailing slash) + =>
+ => + => + => + +// On the command line, if the current directory is /app/dashboard +cd stats # pwd is /app/dashboard/stats +cd ../stats # pwd is /app/stats +cd ../../stats # pwd is /stats +cd ../../../stats # pwd is /stats +``` + +**Note**: The decision to ignore trailing slashes while matching and creating relative paths was not taken lightly by our team. We consulted with a number of our friends and clients (who are also our friends!) about it. We found that most of us don't even understand how plain HTML relative links are handled with the trailing slash. Most people guessed it worked like `cd` on the command line (it does not). Also, HTML relative links don't have the concept of nested routes, they only worked on the URL, so we had to blaze our own trail here a bit. `@reach/router` set this precedent and it has worked out well for a couple of years. + +In addition to ignoring trailing slashes in the current URL, it is important to note that `` will not always behave like `` when your `` matches more than one segment of the URL. Instead of removing just one segment of the URL, **it will resolve based upon the parent route's path, essentially removing all path segments specified by that route**. + +```tsx +function App() { + return ( + + + + } + /> + + + ); +} +``` + +This may seem like an odd choice, to make `..` operate on routes instead of URL segments, but it's a **huge** help when working with `*` routes where an indeterminate number of segments may be matched by the `*`. In these scenarios, a single `..` segment in your `` value can essentially remove anything matched by the `*`, which lets you create more predictable links in `*` routes. + +```tsx +function App() { + return ( + + + } /> + + } + /> + + + ); +} +``` + +## Pass `` state as separate prop + +The `Link` component in v6 accepts `state` as a separate prop instead of receiving it as part of the object passed to `to` so you'll need to update your `Link` components if they are using `state`: + +```js +import { Link } from "react-router-dom"; + +// Change this: + + +// to this: + +``` + +The state value is still retrieved in the linked component using `useLocation()`: + +```js +function Home() { + const location = useLocation(); + const state = location.state; + return
Home
; +} +``` + +## Use `useRoutes` instead of `react-router-config` + +All of the functionality from v5's `react-router-config` package has moved into core in v6. If you prefer/need to define your routes as JavaScript objects instead of using React elements, you're going to love this. + +```js +function App() { + let element = useRoutes([ + // These are the same as the props you provide to + { path: '/', element: }, + { path: 'dashboard', element: }, + { + path: 'invoices', + element: , + // Nested routes use a children property, which is also + // the same as + children: [ + { path: ':id', element: }, + { path: 'sent', element: }, + ], + }, + // Not found routes work as you'd expect + { path: '*', element: }, + ]); + + // The returned element will render the entire element + // hierarchy with all the appropriate context it needs + return element; +} +``` + +Routes defined in this way follow all of the same semantics as ``. In fact, `` is really just a wrapper around `useRoutes`. + +We encourage you to give both `` and `useRoutes` a shot and decide for yourself which one you prefer to use. Honestly, we like and use them both. + +If you had cooked up some of your own logic around data fetching and rendering server-side, we have a low-level `matchRoutes` function available as well similar to the one we had in react-router-config. + +## Use `useNavigate` instead of `useHistory` + +React Router v6 introduces a new navigation API that is synonymous with `` and provides better compatibility with suspense-enabled apps. We include both imperative and declarative versions of this API depending on your style and needs. + +```js +// This is a React Router v5 app +import { useHistory } from 'react-router-dom'; + +function App() { + let history = useHistory(); + function handleClick() { + history.push('/home'); + } + return ( +
+ +
+ ); +} +``` + +In v6, this app should be rewritten to use the `navigate` API. Most of the time this means changing `useHistory` to `useNavigate` and changing the `history.push` or `history.replace` callsite. + +```js +// This is a React Router v6 app +import { useNavigate } from 'react-router-dom'; + +function App() { + let navigate = useNavigate(); + function handleClick() { + navigate('/home'); + } + return ( +
+ +
+ ); +} +``` + +If you need to replace the current location instead of push a new one onto the history stack, use `navigate(to, { replace: true })`. If you need state, use `navigate(to, { state })`. You can think of the first argument to `navigate` as your `` and the other arguments as the `replace` and `state` props. + +If you prefer to use a declarative API for navigation (ala v5's `Redirect` component), v6 provides a `Navigate` component. Use it like: + +```js +import { Navigate } from 'react-router-dom'; + +function App() { + return ; +} +``` + +**Note**: Be aware that the v5 `` uses `replace` logic by default (you may change it via `push` prop), on the other hand, the v6 `` uses `push` logic by default and you may change it via `replace` prop. + +```js +// Change this: + + + +// to this: + + +``` + +If you're currently using `go`, `goBack` or `goForward` from `useHistory` to navigate backwards and forwards, you should also replace these with `navigate` with a numerical argument indicating where to move the pointer in the history stack. For example, here is some code using v5's `useHistory` hook: + +```js +// This is a React Router v5 app +import { useHistory } from 'react-router-dom'; + +function App() { + const { go, goBack, goForward } = useHistory(); + + return ( + <> + + + + + + ); +} +``` + +Here is the equivalent app with v6: + +```js +// This is a React Router v6 app +import { useNavigate } from 'react-router-dom'; + +function App() { + const navigate = useNavigate(); + + return ( + <> + + + + + + ); +} +``` + +Again, one of the main reasons we are moving from using the `history` API directly to the `navigate` API is to provide better compatibility with React suspense. React Router v6 uses the `useNavigation` hook at the root of your component hierarchy. This lets us provide a smoother experience when user interaction needs to interrupt a pending route navigation, for example when they click a link to another route while a previously-clicked link is still loading. The `navigate` API is aware of the internal pending navigation state and will do a REPLACE instead of a PUSH onto the history stack, so the user doesn't end up with pages in their history that never actually loaded. + +_Note: The `` element from v5 is no longer supported as part of your route config (inside a ``). This is due to upcoming changes in React that make it unsafe to alter the state of the router during the initial render. If you need to redirect immediately, you can either a) do it on your server (probably the best solution) or b) render a `` element in your route component. However, recognize that the navigation will happen in a `useEffect`._ + +Aside from suspense compatibility, `navigate`, like `Link`, supports relative navigation. For example: + +```jsx +// assuming we are at `/stuff` +function SomeForm() { + let navigate = useNavigate(); + return ( +
{ + let newRecord = await saveDataFromForm(event.target); + // you can build up the URL yourself + navigate(`/stuff/${newRecord.id}`); + // or navigate relative, just like Link + navigate(`${newRecord.id}`); + }} + > + {/* ... */} +
+ ); +} +``` + +## Remove `` `component` prop + +`` no longer supports the `component` prop for overriding the returned anchor tag. There are a few reasons for this. + +First of all, a `` should pretty much always render an `
`. If yours does not, there's a good chance your app has some serious accessibility and usability problems, and that's no good. The browsers give us a lot of nice usability features with `` and we want your users to get those for free! + +That being said, maybe your app uses a CSS-in-JS library, or maybe you have a custom, fancy link component already in your design system that you'd like to render instead. The `component` prop may have worked well enough in a world before hooks, but now you can create your very own accessible `Link` component with just a few of our hooks: + +```tsx +import { FancyPantsLink } from '@fancy-pants/design-system'; +import { useHref, useLinkClickHandler } from 'react-router-dom'; + +const Link = React.forwardRef(({ onClick, replace = false, state, target, to, ...rest }, ref) => { + let href = useHref(to); + let handleClick = useLinkClickHandler(to, { + replace, + state, + target, + }); + + return ( + { + onClick?.(event); + if (!event.defaultPrevented) { + handleClick(event); + } + }} + ref={ref} + target={target} + /> + ); +}); +``` + +If you're using `react-router-native`, we provide `useLinkPressHandler` that works basically the same way. Just call that hook's returned function in your `Link`'s `onPress` handler and you're all set. + +## Rename `` to `` + +This is a simple renaming of a prop to better align with the common practices of other libraries in the React ecosystem. + +## Remove `activeClassName` and `activeStyle` props from `` + +As of `v6.0.0-beta.3`, the `activeClassName` and `activeStyle` props have been removed from `NavLinkProps`. Instead, you can pass a function to either `style` or `className` that will allow you to customize the inline styling or the class string based on the component's active state. + +```diff tsx + ({ color: isActive ? 'green' : 'blue' })} +> + Messages + +``` + +```diff tsx + "nav-link" + (isActive ? " activated" : "")} +> + Messages + +``` + +If you prefer to keep the v5 props, you can create your own `` as a wrapper component for a smoother upgrade path. + +```tsx +import * as React from 'react'; +import { NavLink as BaseNavLink } from 'react-router-dom'; + +const NavLink = React.forwardRef(({ activeClassName, activeStyle, ...props }, ref) => { + return ( + + [props.className, isActive ? activeClassName : null].filter(Boolean).join(' ') + } + style={({ isActive }) => ({ + ...props.style, + ...(isActive ? activeStyle : null), + })} + /> + ); +}); +``` + +## Get `StaticRouter` from `react-router-dom/server` + +The `StaticRouter` component has moved into a new bundle: `react-router-dom/server`. + +```js +// change +import { StaticRouter } from 'react-router-dom'; +// to +import { StaticRouter } from 'react-router-dom/server'; +``` + +This change was made both to follow more closely the convention established by the `react-dom` package and to help users understand better what a `` is for and when it should be used (on the server). + +## Replace `useRouteMatch` with `useMatch` + +`useMatch` is very similar to v5's `useRouteMatch`, with a few key differences: + +- It uses our new [path pattern matching algorithm](#note-on-route-path-patterns) +- The pattern argument is now required +- No longer accepts an array of patterns +- When passing a pattern as an object, some of the options have been renamed to better align with other APIs in v6 + - `useRouteMatch({ strict })` is now `useMatch({ end })` + - `useRouteMatch({ sensitive })` is now `useMatch({ caseSensitive })` +- It returns a match object with a different shape + +To see the exact API of the new `useMatch` hook and its type declaration, check out our [API Reference](../hooks/use-match). + + + +## Change the order of arguments passed to `matchPath`. Change pathPattern options. + +Since version 6 the order of arguments passed to `matchPath` function has changed. Also pattern options has changed. + +- first argument is pathPattern object, then comes pathname +- pathPattern doesn't include `exact` and `strict` options any more. New `caseSensitive` and `end` options has been added. + +Please refactor it as follows: + +Before: + +```js +// This is a React Router v5 app +import { matchPath } from 'react-router-dom'; + +const match = matchPath('/users/123', { + path: '/users/:id', + exact: true, // Optional, defaults to false + strict: false, // Optional, defaults to false +}); +``` + +After: + +```js +// This is a React Router v6 app +import { matchPath } from 'react-router-dom'; + +const match = matchPath( + { + path: '/users/:id', + caseSensitive: false, // Optional, `true` == static parts of `path` should match case + end: true, // Optional, `true` == pattern should match the entire URL pathname + }, + '/users/123', +); +``` + +## `` is not currently supported + +`` from v5 (along with `usePrompt` and `useBlocker` from the v6 betas) are not included in the current released version of v6. We decided we'd rather ship with what we have than take even more time to nail down a feature that isn't fully baked. We will absolutely be working on adding this back in to v6 at some point in the near future, but not for our first stable release of 6.x. + +## What did we miss? + +Despite our best attempts at being thorough, it's very likely that we missed something. If you follow this upgrade guide and find that to be the case, please let us know. We are happy to help you figure out what to do with your v5 code to be able to upgrade and take advantage of all of the cool stuff in v6. + +Good luck 🤘