Skip to content

Commit

Permalink
feat(react): upgrade to React 18
Browse files Browse the repository at this point in the history
BREAKING CHANGE: react-router API has changed, and @testing-library/react-hooks has been deprecated (See: https://nx.dev/guides/react-18)
  • Loading branch information
jaysoo authored and Jack Hsu committed Apr 19, 2022
1 parent 4b846e8 commit f045dd9
Show file tree
Hide file tree
Showing 19 changed files with 544 additions and 61 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
108 changes: 108 additions & 0 deletions docs/shared/guides/react-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# 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.

**Note:** If you use npm v7/v8, you will need to use `npm install --force` after running `nx migrate 14.0.0` since `@testing-library/react-hook` does not support React 18. Don't worry, this package will be removed in the migration.

## 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. The good news is that `@testing-library/react` (RTL) now comes with its own
`renderHook` utility function since version 13.1.0.

Nx will migrate your code to import `renderHook` from `@testing-library/react` instead of the deprecated package. There are a couple of
utility functions missing from the RTL package: `waitForNextUpdate` and `waitForValueToChange`. If you use either of these
utility functions, try swapping them with `waitFor` instead.

If you continue to have issues after the migration, please open an issue on the RTL repo: https://github.com/testing-library/react-testing-library.
57 changes: 57 additions & 0 deletions packages/react/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@
"version": "13.10.0-beta.0",
"description": "Update to React 18",
"factory": "./src/migrations/update-13-10-0/update-13-10-0"
},
"update-react-dom-render-14.0.0": {
"cli": "nx",
"version": "14.0.0-beta.0",
"description": "Update to React DOM render call to React 18 API.",
"factory": "./src/migrations/update-14-0-0/update-react-dom-render-for-v18"
},
"replace-testing-library-react-hook-14.0.0": {
"cli": "nx",
"version": "14.0.0-beta.0",
"description": "Replace deprecated '@testing-library/react-hook' package with `renderHook` from '@testing-library/react'.",
"factory": "./src/migrations/update-14-0-0/replace-testing-library-react-hook"
}
},
"packageJsonUpdates": {
Expand Down Expand Up @@ -324,6 +336,51 @@
"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.1",
"alwaysAddToPackageJson": false
},
"@emotion/babel-plugin": {
"version": "11.9.2",
"alwaysAddToPackageJson": false
},
"react-router-dom": {
"version": "6.3.0",
"alwaysAddToPackageJson": false
},
"@types/react-dom": {
"version": "18.0.0",
"alwaysAddToPackageJson": false
},
"@testing-library/react": {
"version": "13.1.1",
"alwaysAddToPackageJson": false
},
"react-redux": {
"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,16 +1,18 @@
import { act, renderHook } from '@testing-library/react-hooks';
import <%= hookName %> from './<%= fileName %>';

describe('<%= hookName %>', () => {
it('should render successfully', () => {
const { result } = renderHook(() => <%= hookName %>());

expect(result.current.count).toBe(0);

act(() => {
result.current.increment()
});

expect(result.current.count).toBe(1);
});
import { act, renderHook } from '@testing-library/react';
import * as React from 'react';

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

describe('<%= hookName %>', () => {
it('should render successfully', () => {
const { result } = renderHook(() => <%= hookName %>());

expect(result.current.count).toBe(0);

act(() => {
result.current.increment()
});

expect(result.current.count).toBe(1);
});
});
14 changes: 7 additions & 7 deletions packages/react/src/generators/hook/hook.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('hook', () => {
appTree.exists('libs/my-lib/src/lib/use-form/use-form.ts')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/use-form/use-form.spec.ts')
appTree.exists('libs/my-lib/src/lib/use-form/use-form.spec.tsx')
).toBeTruthy();
});

Expand All @@ -44,7 +44,7 @@ describe('hook', () => {
appTree.exists('apps/my-app/src/app/use-form/use-form.ts')
).toBeTruthy();
expect(
appTree.exists('apps/my-app/src/app/use-form/use-form.spec.ts')
appTree.exists('apps/my-app/src/app/use-form/use-form.spec.tsx')
).toBeTruthy();
});

Expand All @@ -59,7 +59,7 @@ describe('hook', () => {
appTree.exists('libs/my-lib/src/lib/use-hello/use-hello.ts')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/use-hello/use-hello.spec.ts')
appTree.exists('libs/my-lib/src/lib/use-hello/use-hello.spec.tsx')
).toBeFalsy();
});

Expand All @@ -73,7 +73,7 @@ describe('hook', () => {
appTree.exists('libs/my-lib/src/lib/use-hello/use-hello.ts')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/use-hello/use-hello.spec.ts')
appTree.exists('libs/my-lib/src/lib/use-hello/use-hello.spec.tsx')
).toBeFalsy();
});

Expand Down Expand Up @@ -114,7 +114,7 @@ describe('hook', () => {
appTree.exists('libs/my-lib/src/lib/use-hello/use-hello.ts')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/use-hello/use-hello.spec.ts')
appTree.exists('libs/my-lib/src/lib/use-hello/use-hello.spec.tsx')
).toBeFalsy();
});
});
Expand All @@ -130,7 +130,7 @@ describe('hook', () => {
appTree.exists('libs/my-lib/src/lib/use-hello/useHello.ts')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/use-hello/useHello.spec.ts')
appTree.exists('libs/my-lib/src/lib/use-hello/useHello.spec.tsx')
).toBeTruthy();
});
});
Expand All @@ -147,7 +147,7 @@ describe('hook', () => {
appTree.exists('libs/my-lib/src/lib/useHello/useHello.ts')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/useHello/useHello.spec.ts')
appTree.exists('libs/my-lib/src/lib/useHello/useHello.spec.tsx')
).toBeTruthy();
});
});
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

0 comments on commit f045dd9

Please sign in to comment.