Skip to content

Commit

Permalink
feat(react): upgrade to react-router v6
Browse files Browse the repository at this point in the history
BREAKING CHANGE: react-router API has changed (See migration guide: https://reactrouter.com/docs/en/v6/upgrading/v5)
  • Loading branch information
jaysoo authored and Jack Hsu committed Apr 15, 2022
1 parent 4b846e8 commit 5992c4d
Show file tree
Hide file tree
Showing 16 changed files with 513 additions and 39 deletions.
5 changes: 5 additions & 0 deletions docs/map.json
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,11 @@
"id": "using-tailwind-css-in-react",
"file": "shared/guides/using-tailwind-css-in-react"
},
{
"name": "React 18 Migration",
"id": "react-18",
"file": "shared/guides/react-18"
},
{
"name": "Using Tailwind CSS with Angular projects",
"id": "using-tailwind-css-with-angular-projects",
Expand Down
137 changes: 137 additions & 0 deletions docs/shared/guides/react-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# React 18 Migration

[React 18](https://reactjs.org/blog/2022/03/29/react-v18.html) released with many new features, such as Concurrent React, Suspense, batched updates, and more.

Workspaces that upgrade to Nx 14 will be automatically migrated to React 18. This migration will also include an upgrade to React Router v6, if it is used in the workspace, as well as the removal of the deprecated `@testing-library/react-hook` package. Keep reading for more details.

## New `react-dom/client` API

Nx will automatically update your applications to use the new `react-dom-/client` API.

From this:

```typescript jsx
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom';
import App from './app/app';

ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
document.getElementById('root')
);
```

To this:

```typescript jsx
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';
import App from './app/app';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<StrictMode>
<App />
</StrictMode>
);
```

There might be additional changes needed for your code to be fully compatible with React 18. If you use `React.FC` type (which Nx does not use), then you will need to
update your component props to include `children` explicitly.

Before:

```typescript jsx
interface MyButtonProps {
color: string;
}
```

After:

```typescript jsx
interface MyButtonProps {
color: string;
children?: React.ReactNode; // children is no longer implicitly provided by React.FC
}
```

For more information on React 18 migration, please see the [official guide](https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html).

## React Router v6

In addition to the React 18 migration, Nx will also update your workspace to React Router v6 -- assuming you use React Router v5 previously.
There are breaking changes in React Router v6. Please refer to the official [v5 to v6 guide](https://reactrouter.com/docs/en/v6/upgrading/v5) for details.

We highly recommend teams to upgrade their workspace to v6, but if you choose to opt out and continue to use v5, then you will need to disable React strict mode. Navigation is broken in strict mode for React Router v5 due to a transition issue.

To disable strict mode, open your `main.tsx` file and remove `<Strict>` in your render function.

Before:

```typescript jsx
root.render(
<Strict>
<BrowserRouter>
<App />
</BrowserRouter>
</Strict>
);
```

After (for React Router v5):

```typescript jsx
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
```

## `@testing-library/react-hook` is deprecated

The `@testing-library/react-hook` package provides a `renderHook` function to test custom hooks. Unfortunately, this package
does not support React 18, and has been deprecated. There is a [pull-request](https://github.com/testing-library/react-testing-library/pull/991)
to add this functionality to `@testing-library/react` (RTL), but it has not been released yet as of the writing of this guide.

If you rely on `renderHook` to test your custom hooks, then you will need to add your own implementation to your workspace
until RTL is released with `renderHook`.

You can use the following implementation, and share it with all your custom hook tests.

```typescript jsx
function renderHook(
renderCallback: (props: any) => unknown,
options: any = {}
) {
const { initialProps, wrapper } = options;
const result = React.createRef<any>();

function TestComponent({ renderCallbackProps }: any) {
const pendingResult = renderCallback(renderCallbackProps);

React.useEffect(() => {
(result as any).current = pendingResult;
});

return null;
}

const { rerender: baseRerender, unmount } = render(
<TestComponent renderCallbackProps={initialProps} />,
{ wrapper }
);

function rerender(rerenderCallbackProps: any) {
return baseRerender(
<TestComponent renderCallbackProps={rerenderCallbackProps} />
);
}
return { result, rerender, unmount };
}
```
47 changes: 47 additions & 0 deletions packages/react/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
"version": "13.10.0-beta.0",
"description": "Update to React 18",
"factory": "./src/migrations/update-13-10-0/update-13-10-0"
},
"update-14.0.0": {
"cli": "nx",
"version": "14.0.0-beta.0",
"description": "Update to React 18 and updates React DOM render call.",
"factory": "./src/migrations/update-14-0-0/update-14-0-0"
}
},
"packageJsonUpdates": {
Expand Down Expand Up @@ -324,6 +330,47 @@
"alwaysAddToPackageJson": false
}
}
},
"14.0.0": {
"version": "14.0.0-beta.1",
"packages": {
"react": {
"version": "18.0.0",
"alwaysAddToPackageJson": false
},
"react-dom": {
"version": "18.0.0",
"alwaysAddToPackageJson": false
},
"react-is": {
"version": "18.0.0",
"alwaysAddToPackageJson": false
},
"react-test-renderer": {
"version": "18.0.0",
"alwaysAddToPackageJson": false
},
"@types/react": {
"version": "18.0.5",
"alwaysAddToPackageJson": false
},
"@emotion/babel-plugin": {
"version": "11.9.2",
"alwaysAddToPackageJson": false
},
"react-router-dom": {
"version": "6.3.0",
"alwaysAddToPackageJson": false
},
"@testing-library/react": {
"version": "13.0.1",
"alwaysAddToPackageJson": false
},
"@testing-library/react-hooks": {
"version": "8.0.0",
"alwaysAddToPackageJson": false
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<% if (strict) { %>import { StrictMode } from 'react';<% } %>
import * as ReactDOMClient from 'react-dom/client';
import * as ReactDOM from 'react-dom/client';
<% if (routing) { %>import { BrowserRouter } from 'react-router-dom';<% } %>

import App from './app/<%= fileName %>';

const root = ReactDOMClient.createRoot(document.getElementById('root') as HTMLElement);
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<% if (strict) { %><StrictMode><% } %><% if (routing) { %><BrowserRouter><% } %><App /><% if (routing) { %></BrowserRouter><% } %><% if (strict) { %></StrictMode><% } %>
);
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class <%= className %> extends Component<<%= className %>Props> {
<ul>
<li><Link to="/"><%= name %> root</Link></li>
</ul>
<Route path="/" render={() => <div>This is the <%= name %> root route.</div>} />
<Route path="/" element={<div>This is the <%= name %> root route.</div>} />
<% } %>
</<%= wrapper %>>
);
Expand All @@ -59,7 +59,7 @@ export function <%= className %>(props: <%= className %>Props) {
<ul>
<li><Link to="/"><%= name %> root</Link></li>
</ul>
<Route path="/" render={() => <div>This is the <%= name %> root route.</div>} />
<Route path="/" element={<div>This is the <%= name %> root route.</div>} />
<% } %>
</<%= wrapper %>>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,44 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { act, render } from '@testing-library/react';
import * as React from 'react';

import <%= hookName %> from './<%= fileName %>';

// Temporarily inline the renderHook function until it is used into RTL.
//
// It is recommended to extract this to be shared for each hook test file, and removed once RTL provides it.
//
// See: https://github.com/testing-library/react-hooks-testing-library/issues/654#issuecomment-1097276573
// PR for RTL: https://github.com/testing-library/react-testing-library/pull/991
function renderHook(
renderCallback: (props: any) => unknown,
options: any = {}
) {
const { initialProps, wrapper } = options;
const result = React.createRef<any>();

function TestComponent({ renderCallbackProps }: any) {
const pendingResult = renderCallback(renderCallbackProps);

React.useEffect(() => {
(result as any).current = pendingResult;
});

return null;
}

const { rerender: baseRerender, unmount } = render(
<TestComponent renderCallbackProps={initialProps} />,
{ wrapper }
);

function rerender(rerenderCallbackProps: any) {
return baseRerender(
<TestComponent renderCallbackProps={rerenderCallbackProps} />
);
}
return { result, rerender, unmount };
}

describe('<%= hookName %>', () => {
it('should render successfully', () => {
const { result } = renderHook(() => <%= hookName %>());
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/generators/hook/hook.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// TODO(jack): Remove inline renderHook function when RTL releases with its own version
import * as ts from 'typescript';
import {
applyChangesToString,
Expand Down
2 changes: 0 additions & 2 deletions packages/react/src/generators/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
reactDomVersion,
reactTestRendererVersion,
reactVersion,
testingLibraryReactHooksVersion,
testingLibraryReactVersion,
typesReactDomVersion,
typesReactVersion,
Expand Down Expand Up @@ -61,7 +60,6 @@ function updateDependencies(host: Tree) {
'@types/react': typesReactVersion,
'@types/react-dom': typesReactDomVersion,
'@testing-library/react': testingLibraryReactVersion,
'@testing-library/react-hooks': testingLibraryReactHooksVersion,
'react-test-renderer': reactTestRendererVersion,
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import NxWelcome from "./nx-welcome";
<% if (remotes.length > 0) { %>
import { Link, Route, Switch } from 'react-router-dom';
import { Link, Route, Routes } from 'react-router-dom';

<% remotes.forEach(function(r) { %>
const <%= r.className %> = React.lazy(() => import('<%= r.fileName %>/Module'));
Expand All @@ -16,12 +16,12 @@ export function App() {
<li><Link to="/<%=r.fileName%>"><%=r.className%></Link></li>
<% }); %>
</ul>
<Switch>
<Route exact path="/" render={() => <NxWelcome title="<%= projectName %>"/>} />
<Routes>
<Route path="/" element={<NxWelcome title="<%= projectName %>"/>} />
<% remotes.forEach(function(r) { %>
<Route path="/<%=r.fileName%>" render={() => <<%= r.className %>/>} />
<Route path="/<%=r.fileName%>" element={<<%= r.className %>/>} />
<% }); %>
</Switch>
</Routes>
</React.Suspense>
);
}
Expand Down
18 changes: 9 additions & 9 deletions packages/react/src/mfe/mfe-ast-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe('addRemoteRoute', () => {
it('should add remote route to host app', async () => {
const sourceCode = stripIndents`
import * as React from 'react';
import { Link, Route, Switch } from 'react-router-dom';
import { Link, Route, Routes } from 'react-router-dom';
const App1 = React.lazy(() => import('app1/Module'));
Expand All @@ -127,9 +127,9 @@ describe('addRemoteRoute', () => {
<ul>
<li><Link to="/app1">App1</Link></li>
</ul>
<Switch>
<Route path="/app1" render={() => <App1 />} />
</Switch>
<Routes>
<Route path="/app1" element={<App1 />} />
</Routes>
</React.Suspense>
);
}
Expand All @@ -152,7 +152,7 @@ describe('addRemoteRoute', () => {
expect(result).toEqual(
stripIndents`
import * as React from 'react';
import { Link, Route, Switch } from 'react-router-dom';
import { Link, Route, Routes } from 'react-router-dom';
const App2 = React.lazy(() => import('app2/Module'));
Expand All @@ -165,10 +165,10 @@ describe('addRemoteRoute', () => {
<li><Link to="/app1">App1</Link></li>
<li><Link to="/app2">App2</Link></li>
</ul>
<Switch>
<Route path="/app1" render={() => <App1 />} />
<Route path="/app2" render={() => <App2 />} />
</Switch>
<Routes>
<Route path="/app1" element={<App1 />} />
<Route path="/app2" element={<App2 />} />
</Routes>
</React.Suspense>
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/mfe/mfe-ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function addRemoteRoute(
changes.push({
type: ChangeType.Insert,
index: firstRoute.end,
text: `\n<Route path="/${names.fileName}" render={() => <${names.className} />} />`,
text: `\n<Route path="/${names.fileName}" element={<${names.className} />} />`,
});

if (firstLink) {
Expand Down

0 comments on commit 5992c4d

Please sign in to comment.