# `react-router` upgrade
**Current:**`react-router @^3.2.0` (5 years ago)
**Upgrade to:** `react-router @latest`
## 🤩 New from v3 to v6 (6.4+)
- `react-router` breaks down to
- `react-router-dom` (for web) - it's all we need!
- `react-router-native` (for react native)
- `<Router>` representation as a certain category, e.g., `BrowserRouter`, `HashRouter`
- more detail [here](https://reactrouter.com/en/main/routers/picking-a-router)
- use **hooks** to share all the router's internal state
- ✨ Data loading during a navigation...
```javascript=
<Route
path=":gameId"
loader={({ params }) => {
// of course you can use any data store
return fakeSdk.getTeam(params.gameId);
}}
element={<Game />}
/>
function Game() {
const game = useLoaderData();
// data from <Route path=":gameId">
}
```
- Pending Naviation
```javascript=
function Root() {
const navigation = useNavigation();
return (
<div>
{navigation.state === "loading" && <GlobalSpinner />}
<FakeSidebar />
<Outlet />
<FakeFooter />
</div>
);
}
```
- Skeletion UI with `<Suspense/>`
```jsx=
<Route
path="issue/:issueId"
element={<Issue />}
loader={async ({ params }) => {
// these are promises, but *not* awaited
const history = fake.getIssueHistory(params.issueId);
// the issue, however, *is* awaited
const issue = await fake.getIssue(params.issueId);
// defer enables suspense for the un-awaited promises
return defer({ issue, history });
}}
/>;
function Issue() {
const { issue, history } = useLoaderData();
return (
<IssueDescription issue={issue} />
<Suspense fallback={<IssueHistorySkeleton />}>
<Await resolve={history}>
{(resolvedHistory) => (<IssueHistory history={resolvedHistory} />)}
{/* or you can use hooks to access the data (e.g., <IssueHistory />) */}
</Await>
</Suspense>
);
}
function IssueHistory() {
const history = useAsyncValue();
return <div>{/* ... */}</div>;
}
```
- Route matching...
- Ranked Route Matching (e.g.,`/teams/new`)
- Layout Routes (e.g., `/privacy`)
```jsx=
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path=":teamId/edit" element={<EditTeam />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route>
</Route>
<Route element={<PageLayout />}>
<Route path="/privacy" element={<Privacy />} />
<Route path="/tos" element={<Tos />} />
</Route>
<Route path="contact-us" element={<Contact />} />
</Routes>
```
- .... [& more](https://reactrouter.com/en/main/start/overview#nested-routes)
## 🤯 Code Changes
### `<Router />`
#### v3
Primary component of React Router to keep UI and the URL in sync. (which listen to `history`)
```jsx=
import { hashHistory } from 'react-router'
<Router history={hashHistory}>
...
</Router>
```
#### v6
New routers were introduced that support the new data APIs. [(ref)](https://reactrouter.com/en/main/routers/picking-a-router)
```jsx=
const router = createHashRouter([
{
path: "/",
element: <DashboardLayout />,
children: [{...}],
},
])
// or use `createRoutesFromElements`
const routes = createRoutesFromElements(
<Route element={<DashboardLayout />} path="/">
...
</Route>
)
const router = createHashRouter(routes);
<RouterProvider router={router} />
```
### `<Route/>` - related API Change
#### v3
```jsx=
<Route
component={CreateBrainSelectMultiSegmentSource}
path="base"
title={t`Importing Audience Candidates`}
/>
<IndexRoute component={ConnectionSourceDetail} />
<IndexRedirect to="/data/list" />
<Route
component={Insight}
path="insight"
onEnter={() => {
checkNotification();
checkRobotUsage();
}}
/>
```
#### v6
([ref](https://reactrouter.com/en/main/upgrading/v5#advantages-of-route-element): Advantages of using `<Route element>`)
```jsx=
<Route
element={
<CreateBrainSelectMultiSegmentSource
title={t`Importing Audience Candidates`}
/>
}
path="base"
/>
<Route index element={<ConnectionSourceDetail />} />
<Route index element={<Navigate replace to="data/list" />} />
<Route
element={<Insight />}
loader={() => {
checkNotification();
checkRobotUsage();
return null;
}}
path="insight"
/>
```
### RegExp-style route
#### v3
```jsx=
<Route
component={DataList}
path="(connection-source-)data/list"
onEnter={() => {
checkNotification();
checkRobotUsage();
}}
/>
```
#### v6
- RegExp-style route is not supported [(ref)](https://reactrouter.com/en/main/start/faq#what-happened-to-regexp-routes-paths).
- `<Route path>` in v6 supports only 2 kinds of placeholders: dynamic `:id`-style params and `*` wildcards. [(ref)](https://reactrouter.com/en/main/upgrading/v5#note-on-route-path-patterns)
```jsx=
<Route
path="data/list"
...
/>
<Route
path="connection-source-data/list"
...
/>
```
### Nested Route
#### v3
(quite weird actually 🤔)
```jsx=
<Route component={PredictionList} path="prediction/list">
<Route
component={PredictionDetail}
path="/prediction/detail/:id"
onEnter={checkAttributeUsage}
>
<Route component={AdvanceCreateAudience} path="segment/create" />
</Route>
<Route component={PredictionSetup} path="/prediction/setup/:id" />
</Route>
```
#### v6
Use relatvie child route path. Or an absolute child route path must start with the combined path of all its parent routes.
```jsx=
<Route element={<PredictionList />} path="prediction/list" />
<Route element={<PredictionSetup />} path="prediction/setup/:id" />
<Route
element={<PredictionDetail />}
loader={() => {
checkAttributeUsage();
return null;
}}
path="prediction/detail/:id"
>
<Route element={<AdvanceCreateAudience />} path="segment/create" />
</Route>
```
### `<Outlet />`
For parent route elements to render their child route elements:
```jsx=
<Route path="/" element={<DashboardLayout />}>
<Route element={<SsoLogin />} path="sso_login" />
</Route>
```
#### v3
```jsx=
function DashboardLayout({children}) {
return (
<div>
{children}
</div>
);
}
```
#### v6
```jsx=
function DashboardLayout() {
return (
<div>
<Outlet />
</div>
);
}
```
Also, for parent routes to manage state or other values and to share with child routes:
```jsx=
function Parent() {
const [count, setCount] = React.useState(0);
return <Outlet context={[count, setCount]} />;
}
import { useOutletContext } from "react-router-dom";
function Child() {
const [count, setCount] = useOutletContext();
const increment = () => setCount((c) => c + 1);
return <button onClick={increment}>{count}</button>;
}
```
### `withRouter` HOC
#### v3
Use the bulit in `withRouter` HOC to pass the router props to the React components.
```javascript=
withRouterProps = {
router,
params,
location,
routes
}
```
#### v6
However, in v6, they use **hooks** to share all the router's internal state. So we will need a wrapper for class components to use them [(ref)](https://reactrouter.com/en/main/start/faq#what-happened-to-withrouter-i-need-it)
```jsx=
function withRouter(Component) {
function ComponentWithRouterProp(props) {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
return (
<Component
location={location}
navigate={navigate}
params={params}
{...props}
/>
);
}
return ComponentWithRouterProp;
}
withRouterProps = {
location: ReturnType<typeof useLocation>;
params: Record<string, string>;
navigate: ReturnType<typeof useNavigate>;
}
```
### Navigation
#### v3
```javascript=
router.push(`/prediction/detail/${id}`)
router.replace('/prediction/list')
```
#### v6
Replace the current location or push a new one onto the history with `navigate` [(ref)](https://reactrouter.com/en/main/hooks/use-navigate)
```javascript=
const navigate = useNavigate();
navigate(`/prediction/detail/${id})
navigate('/prediction/list', {
replace: true
})
```
:::spoiler
> 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.
> -- ref: https://reactrouter.com/en/main/upgrading/v5#use-usenavigate-instead-of-usehistory
:::
##### Navigation outside a react compoents
[ref](https://github.com/remix-run/react-router/issues/9422#issuecomment-1301182219)
```javascript=
export const router = createHashRouter(...);
router.navigate('/path');
```
### Active Links
#### v3
```jsx=
<Link
activeClassName={cx('is-active')}
className={cx('item', { 'is-active': active })}
to={to}
{...linkProps}
>
{children}
</Link>
```
#### v6
A `<NavLink>` is a special kind of `<Link>` that knows whether or not it is "active". [(ref)](https://reactrouter.com/en/main/upgrading/v5#remove-activeclassname-and-activestyle-props-from-navlink-)
```jsx=
<NavLink
end // to ensure it's an exact match
className={({ isActive }) =>
isActive ? cx('is-active', 'item') : cx('item')
}
to={to}
{...linkProps}
>
{children}
</NavLink>
```
### Check for Active outside of links
#### v3
```javascript=
const isCurrentlyActive = router.isActive(`/audience/detail/${id}`, true);
```
#### v6
use `matchPath` matches a route path pattern against a URL pathname and returns information about the match.
```javascript=
const isCurrentlyActive = !!matchPath(
{
path: location.pathname,
end: true, // exact match
},
`/audience/detail/${id}`
);
```
There's also a hook version `useMatch` allow us to uses this function internally to match a route path relative to the current location. [(ref)](https://reactrouter.com/en/main/start/overview#active-links)
```javascript=
const match = useMatch('/connection/list')
const isCurrentlyActive = Boolean(match)
```
### Get Query from URL
#### v3
```javascript=
const { tab } = location.query;
```
#### v6
`query` in location object has been replaced by `search`
```javascript=
{
pathname: "/bbq/pig-pickins",
search: "?campaign=instagram",
hash: "#menu",
state: null,
key: "aefz24ie"
}
```
```javascript=
const location = useLocation();
const tab = new URLSearchParams(location.search).get('tab');
```
## 🤔 Discussion
### Code review
For this kind of BIG change, should all changes be in one commit? Or should it be more specific?
➡️ One commit is fine. Note changes in doc to help review.
### `react-router` + `redux`
Currently, we use `react-router-redux` to manage the state of history. Since the package is deprecated, we should probaby use `redux-first-history` instead (which support react-router v6). **However, do we still need redux to manage routes?**
➡️ Remove `react-router-redux`.
### `router.setRouteLeaveHook()`
Currently, we use this v3 API to handle
```javascript=
useEffect(() => {
const routerWillLeave = () => {
if (
isDirty &&
!confirm(/* t: Confirm dialog (OK / Cancel) */ t`Discard new brain?`)
) {
return false;
}
dispatch(resetCreateBrainForm());
dispatch(resetBrainTemplates());
return true;
};
router.setRouteLeaveHook(route, routerWillLeave);
}, [dispatch, isDirty, route, router]);
```
In v4&5, the API has been replaced by `<Prompt>` & `<Block>` ([doc](https://v5.reactrouter.com/core/api/Prompt)). However, in v6, the API has been removed (ref: [doc](https://reactrouter.com/en/main/upgrading/v5#prompt-is-not-currently-supported) / [🔥 github discussion thread](https://github.com/remix-run/react-router/issues/8139) / [browser constraint](https://developer.chrome.com/blog/chrome-51-deprecations/#remove-custom-messages-in-onbeforeunload-dialogs))
➡️ `useBlock` was released with react-router v6.8, which we can use to build our custom `usePrompt` yay 🎉
### Problem with nested routes
With react-router v3, we can use nested route like this:
```jsx
<Route component={PredictionList} path="prediction/list">
<Route
component={PredictionDetail}
path="/prediction/detail/:id"
>
<Route component={AdvanceCreateAudience} path="segment/create" />
</Route>
...
</Route>
```
So when we click on a prediction and render `<PredictionDetail/>`, we will not lose `<PredictionList/>` (so we dont have to wait when we close the `prediction/detail` and go back to `prediction/list`)
However, in react-router v6, we can no longer put an absolute path as a nested route...
```jsx
<Route component={PredictionList} path="prediction/list"/>
<Route
component={PredictionDetail}
path="/prediction/detail/:id"
>
<Route component={AdvanceCreateAudience} path="segment/create" />
</Route>
```
As I currently cannot find a way to bypass the restriction, one of the intuitive solutions could be:
```jsx
<Route component={PredictionList} path="prediction/list"/>
<Route component={PredictionList} path="prediction">
<Route
component={PredictionDetail}
path="detail/:id"
>
<Route component={AdvanceCreateAudience} path="segment/create" />
</Route>
</Route>
```
However, one of the fallback is that we now create a new route: `/prediction` 😬
# Test Failed
```cmd
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
Here's what you can do:
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs: https://jestjs.io/docs/en/configuration.html
```
```cmd
Details:
/Users/emily.yn.chen/Appier/segment-dashboard-2/node_modules/@appier/aiqua-design-system/themes/index.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){export * from '../dist/esm/themes';
^^^^^^
SyntaxError: Unexpected token 'export'
5 | import type EnAnalyticsUI from '@appier/analytics-ui';
6 |
> 7 | import { THEME_TYPES } from '@appier/aiqua-design-system/themes';
| ^
8 | import { useTheme } from 'components/common/theme_components/context';
9 | import aixonLogo from 'components/pages/HomeScreen/images/logo-aixon.svg';
10 | import { useAppSelector } from 'hooks/useRedux';
at ScriptTransformer._transformAndBuildScript (node_modules/@jest/transform/build/ScriptTransformer.js:537:17)
at ScriptTransformer.transform (node_modules/@jest/transform/build/ScriptTransformer.js:579:25)
at Object.<anonymous> (src/components/pages/Analytics/Analytics.tsx:7:1)
```