Merge pull request #28680 from schultzp2020/jsx-transformation-guide
docs: add new JSX Transform guide
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Included a reference to the JSX transform guide in the warning about using the default React import.
|
||||
@@ -535,3 +535,4 @@ zsh
|
||||
scrollable
|
||||
severities
|
||||
intellij
|
||||
tsconfig
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
# Migrating to the New JSX Transform using a Codemod
|
||||
|
||||
## Using the Codemod
|
||||
|
||||
While a codemod for the New JSX Transform was originally introduced in the [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) article, it is no longer functional. A working solution, inspired by the original, is detailed below:
|
||||
|
||||
1. **Create the transform file**
|
||||
|
||||
Create a file named `transform.js` in the root directory of your Backstage project.
|
||||
|
||||
```js
|
||||
/**
|
||||
* (c) Facebook, Inc. and its affiliates. Confidential and proprietary.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
module.exports = function (file, api, options) {
|
||||
const j = api.jscodeshift;
|
||||
const printOptions = options.printOptions || {};
|
||||
const root = j(file.source);
|
||||
const destructureNamespaceImports = options.destructureNamespaceImports;
|
||||
|
||||
// <https://github.com/facebook/jscodeshift/blob/master/recipes/retain-first-comment.md>
|
||||
function getFirstNode() {
|
||||
return root.find(j.Program).get('body', 0).node;
|
||||
}
|
||||
|
||||
// Save the comments attached to the first node
|
||||
const firstNode = getFirstNode();
|
||||
const { comments } = firstNode;
|
||||
|
||||
function isVariableDeclared(variable) {
|
||||
return (
|
||||
root
|
||||
.find(j.Identifier, {
|
||||
name: variable,
|
||||
})
|
||||
.filter(
|
||||
path =>
|
||||
path.parent.value.type !== 'MemberExpression' &&
|
||||
path.parent.value.type !== 'QualifiedTypeIdentifier' &&
|
||||
// Added this
|
||||
path.parent.value.type !== 'TSQualifiedName' &&
|
||||
path.parent.value.type !== 'JSXMemberExpression',
|
||||
)
|
||||
.size() > 0
|
||||
);
|
||||
}
|
||||
|
||||
// Get all paths that import from React
|
||||
const reactImportPaths = root
|
||||
.find(j.ImportDeclaration, {
|
||||
type: 'ImportDeclaration',
|
||||
})
|
||||
.filter(path => {
|
||||
return (
|
||||
(path.value.source.type === 'Literal' ||
|
||||
path.value.source.type === 'StringLiteral') &&
|
||||
(path.value.source.value === 'React' ||
|
||||
path.value.source.value === 'react')
|
||||
);
|
||||
});
|
||||
|
||||
// get all namespace/default React imports
|
||||
const reactPaths = reactImportPaths.filter(path => {
|
||||
return (
|
||||
path.value.specifiers.length > 0 &&
|
||||
path.value.importKind === 'value' &&
|
||||
path.value.specifiers.some(
|
||||
specifier => specifier.local.name === 'React',
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
if (reactPaths.size() > 1) {
|
||||
throw Error(
|
||||
'There should only be one React import. Please remove the duplicate import and try again.',
|
||||
);
|
||||
}
|
||||
|
||||
if (reactPaths.size() === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reactPath = reactPaths.paths()[0];
|
||||
// Reuse the node so that we can preserve quoting style.
|
||||
const reactLiteral = reactPath.value.source;
|
||||
|
||||
const isDefaultImport = reactPath.value.specifiers.some(
|
||||
specifier =>
|
||||
specifier.type === 'ImportDefaultSpecifier' &&
|
||||
specifier.local.name === 'React',
|
||||
);
|
||||
|
||||
// Check to see if we should keep the React import
|
||||
const isReactImportUsed =
|
||||
root
|
||||
.find(j.Identifier, {
|
||||
name: 'React',
|
||||
})
|
||||
.filter(path => {
|
||||
return path.parent.parent.value.type !== 'ImportDeclaration';
|
||||
})
|
||||
.size() > 0;
|
||||
|
||||
// local: imported
|
||||
const reactIdentifiers = {};
|
||||
const reactTypeIdentifiers = {};
|
||||
let canDestructureReactVariable = false;
|
||||
if (
|
||||
isReactImportUsed &&
|
||||
(isDefaultImport || destructureNamespaceImports)
|
||||
) {
|
||||
// Checks to see if the react variable is used itself (rather than used to access its properties)
|
||||
canDestructureReactVariable =
|
||||
root
|
||||
.find(j.Identifier, {
|
||||
name: 'React',
|
||||
})
|
||||
.filter(path => {
|
||||
return path.parent.parent.value.type !== 'ImportDeclaration';
|
||||
})
|
||||
.filter(
|
||||
path =>
|
||||
!(
|
||||
path.parent.value.type === 'MemberExpression' &&
|
||||
path.parent.value.object.name === 'React'
|
||||
) &&
|
||||
!(
|
||||
path.parent.value.type === 'QualifiedTypeIdentifier' &&
|
||||
path.parent.value.qualification.name === 'React'
|
||||
) &&
|
||||
!(
|
||||
// Added this
|
||||
(
|
||||
path.parent.value.type === 'TSQualifiedName' &&
|
||||
path.parent.value.left.name === 'React'
|
||||
)
|
||||
) &&
|
||||
!(
|
||||
path.parent.value.type === 'JSXMemberExpression' &&
|
||||
path.parent.value.object.name === 'React'
|
||||
),
|
||||
)
|
||||
.size() === 0;
|
||||
|
||||
if (canDestructureReactVariable) {
|
||||
// Add React identifiers to separate object so we can destructure the imports
|
||||
// later if we can. If a type variable that we are trying to import has already
|
||||
// been declared, do not try to destructure imports
|
||||
// (ex. Element is declared and we are using React.Element)
|
||||
root
|
||||
.find(j.QualifiedTypeIdentifier, {
|
||||
qualification: {
|
||||
type: 'Identifier',
|
||||
name: 'React',
|
||||
},
|
||||
})
|
||||
.forEach(path => {
|
||||
const id = path.value.id.name;
|
||||
if (path.parent.parent.value.type === 'TypeofTypeAnnotation') {
|
||||
// This is a typeof import so it isn't actually a type
|
||||
reactIdentifiers[id] = id;
|
||||
|
||||
if (reactTypeIdentifiers[id]) {
|
||||
canDestructureReactVariable = false;
|
||||
}
|
||||
} else {
|
||||
reactTypeIdentifiers[id] = id;
|
||||
|
||||
if (reactIdentifiers[id]) {
|
||||
canDestructureReactVariable = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isVariableDeclared(id)) {
|
||||
canDestructureReactVariable = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Added this
|
||||
root
|
||||
.find(j.TSQualifiedName, {
|
||||
left: {
|
||||
type: 'Identifier',
|
||||
name: 'React',
|
||||
},
|
||||
})
|
||||
.forEach(path => {
|
||||
const id = path.value.right.name;
|
||||
reactIdentifiers[id] = id;
|
||||
// We don't tend to use type imports
|
||||
// Comment line above out and uncomment this to use type imports
|
||||
// Also ignoring typeof imports?
|
||||
// reactTypeIdentifiers[id] = id
|
||||
|
||||
// if (reactIdentifiers[id]) {
|
||||
// canDestructureReactVariable = false
|
||||
// }
|
||||
|
||||
if (isVariableDeclared(id)) {
|
||||
canDestructureReactVariable = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Add React identifiers to separate object so we can destructure the imports
|
||||
// later if we can. If a variable that we are trying to import has already
|
||||
// been declared, do not try to destructure imports
|
||||
// (ex. createElement is declared and we are using React.createElement)
|
||||
root
|
||||
.find(j.MemberExpression, {
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
name: 'React',
|
||||
},
|
||||
})
|
||||
.forEach(path => {
|
||||
const property = path.value.property.name;
|
||||
reactIdentifiers[property] = property;
|
||||
|
||||
if (
|
||||
isVariableDeclared(property) ||
|
||||
reactTypeIdentifiers[property]
|
||||
) {
|
||||
canDestructureReactVariable = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Add React identifiers to separate object so we can destructure the imports
|
||||
// later if we can. If a JSX variable that we are trying to import has already
|
||||
// been declared, do not try to destructure imports
|
||||
// (ex. Fragment is declared and we are using React.Fragment)
|
||||
root
|
||||
.find(j.JSXMemberExpression, {
|
||||
object: {
|
||||
type: 'JSXIdentifier',
|
||||
name: 'React',
|
||||
},
|
||||
})
|
||||
.forEach(path => {
|
||||
const property = path.value.property.name;
|
||||
reactIdentifiers[property] = property;
|
||||
|
||||
if (
|
||||
isVariableDeclared(property) ||
|
||||
reactTypeIdentifiers[property]
|
||||
) {
|
||||
canDestructureReactVariable = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (canDestructureReactVariable) {
|
||||
// replace react identifiers
|
||||
root
|
||||
.find(j.QualifiedTypeIdentifier, {
|
||||
qualification: {
|
||||
type: 'Identifier',
|
||||
name: 'React',
|
||||
},
|
||||
})
|
||||
.forEach(path => {
|
||||
const id = path.value.id.name;
|
||||
|
||||
j(path).replaceWith(j.identifier(id));
|
||||
});
|
||||
|
||||
// Added this
|
||||
root
|
||||
.find(j.TSQualifiedName, {
|
||||
left: {
|
||||
type: 'Identifier',
|
||||
name: 'React',
|
||||
},
|
||||
})
|
||||
.forEach(path => {
|
||||
const id = path.value.right.name;
|
||||
|
||||
j(path).replaceWith(j.identifier(id));
|
||||
});
|
||||
|
||||
root
|
||||
.find(j.MemberExpression, {
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
name: 'React',
|
||||
},
|
||||
})
|
||||
.forEach(path => {
|
||||
const property = path.value.property.name;
|
||||
|
||||
j(path).replaceWith(j.identifier(property));
|
||||
});
|
||||
|
||||
root
|
||||
.find(j.JSXMemberExpression, {
|
||||
object: {
|
||||
type: 'JSXIdentifier',
|
||||
name: 'React',
|
||||
},
|
||||
})
|
||||
.forEach(path => {
|
||||
const property = path.value.property.name;
|
||||
|
||||
j(path).replaceWith(j.jsxIdentifier(property));
|
||||
});
|
||||
|
||||
// Add exisiting React imports to map
|
||||
reactImportPaths.forEach(path => {
|
||||
const specifiers = path.value.specifiers;
|
||||
for (let i = 0; i < specifiers.length; i++) {
|
||||
const specifier = specifiers[i];
|
||||
// get all type and regular imports that are imported
|
||||
// from React
|
||||
if (specifier.type === 'ImportSpecifier') {
|
||||
if (
|
||||
path.value.importKind === 'type' ||
|
||||
specifier.importKind === 'type'
|
||||
) {
|
||||
reactTypeIdentifiers[specifier.local.name] =
|
||||
specifier.imported.name;
|
||||
} else {
|
||||
reactIdentifiers[specifier.local.name] = specifier.imported.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const regularImports = [];
|
||||
Object.keys(reactIdentifiers).forEach(local => {
|
||||
const imported = reactIdentifiers[local];
|
||||
regularImports.push(
|
||||
j.importSpecifier(j.identifier(imported), j.identifier(local)),
|
||||
);
|
||||
});
|
||||
|
||||
const typeImports = [];
|
||||
Object.keys(reactTypeIdentifiers).forEach(local => {
|
||||
const imported = reactTypeIdentifiers[local];
|
||||
typeImports.push(
|
||||
j.importSpecifier(j.identifier(imported), j.identifier(local)),
|
||||
);
|
||||
});
|
||||
|
||||
if (regularImports.length > 0) {
|
||||
j(reactPath).insertAfter(
|
||||
j.importDeclaration(regularImports, reactLiteral),
|
||||
);
|
||||
}
|
||||
if (typeImports.length > 0) {
|
||||
j(reactPath).insertAfter(
|
||||
j.importDeclaration(typeImports, reactLiteral, 'type'),
|
||||
);
|
||||
}
|
||||
|
||||
// remove all old react imports
|
||||
reactImportPaths.forEach(path => {
|
||||
// This is for import type React from 'react' which shouldn't
|
||||
// be removed
|
||||
if (
|
||||
path.value.specifiers.some(
|
||||
specifier =>
|
||||
specifier.type === 'ImportDefaultSpecifier' &&
|
||||
specifier.local.name === 'React' &&
|
||||
(specifier.importKind === 'type' ||
|
||||
path.value.importKind === 'type'),
|
||||
)
|
||||
) {
|
||||
j(path).insertAfter(
|
||||
j.importDeclaration(
|
||||
[j.importDefaultSpecifier(j.identifier('React'))],
|
||||
reactLiteral,
|
||||
'type',
|
||||
),
|
||||
);
|
||||
}
|
||||
j(path).remove();
|
||||
});
|
||||
} else {
|
||||
// Remove the import because it's not being used
|
||||
// If we should keep the React import, just convert
|
||||
// default imports to named imports
|
||||
let isImportRemoved = false;
|
||||
const specifiers = reactPath.value.specifiers;
|
||||
for (let i = 0; i < specifiers.length; i++) {
|
||||
const specifier = specifiers[i];
|
||||
if (specifier.type === 'ImportNamespaceSpecifier') {
|
||||
if (!isReactImportUsed) {
|
||||
isImportRemoved = true;
|
||||
j(reactPath).remove();
|
||||
}
|
||||
} else if (specifier.type === 'ImportDefaultSpecifier') {
|
||||
if (isReactImportUsed) {
|
||||
j(reactPath).insertAfter(
|
||||
j.importDeclaration(
|
||||
[j.importNamespaceSpecifier(j.identifier('React'))],
|
||||
reactLiteral,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (specifiers.length > 1) {
|
||||
const typeImports = [];
|
||||
const regularImports = [];
|
||||
for (let x = 0; x < specifiers.length; x++) {
|
||||
if (specifiers[x].type !== 'ImportDefaultSpecifier') {
|
||||
if (specifiers[x].importKind === 'type') {
|
||||
typeImports.push(specifiers[x]);
|
||||
} else {
|
||||
regularImports.push(specifiers[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (regularImports.length > 0) {
|
||||
j(reactPath).insertAfter(
|
||||
j.importDeclaration(regularImports, reactLiteral),
|
||||
);
|
||||
}
|
||||
if (typeImports.length > 0) {
|
||||
j(reactPath).insertAfter(
|
||||
j.importDeclaration(typeImports, reactLiteral, 'type'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
isImportRemoved = true;
|
||||
j(reactPath).remove();
|
||||
}
|
||||
}
|
||||
|
||||
if (!isImportRemoved) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// If the first node has been modified or deleted, reattach the comments
|
||||
const firstNode2 = getFirstNode();
|
||||
if (firstNode2 !== firstNode) {
|
||||
firstNode2.comments = comments;
|
||||
}
|
||||
|
||||
return root.toSource(printOptions);
|
||||
};
|
||||
```
|
||||
|
||||
2. **Execute the transformation**
|
||||
|
||||
To apply the necessary changes, execute the following command twice from your root Backstage directory. First, run it for your packages, and then again for your plugins and any additional directories. Remember to adjust the paths to the transform script and parser source accordingly.
|
||||
|
||||
```console
|
||||
npx jscodeshift --verbose=2 --ignore-pattern="**/node_modules/**" --parser ts --extensions=tsx,ts,jsx,js --transform ./path/to/the/transform.js --destructureNamespaceImports=true --parser=tsx ./path/to/src/
|
||||
```
|
||||
|
||||
3. **Verify and clean up imports**
|
||||
|
||||
Review the codebase for any remaining instances of `import * as React from 'react'` or `import React from 'react'`. Replace these with named imports where possible, such as:
|
||||
|
||||
```tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
```
|
||||
|
||||
If retaining the default React import is absolutely necessary, use the following syntax instead:
|
||||
|
||||
```tsx
|
||||
import { default as React } from 'react';
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
id: jsx-transform-migration
|
||||
title: Transitioning to the New JSX Transform
|
||||
description: A guide to migrating your project to the New JSX Transform
|
||||
---
|
||||
|
||||
Backstage core libraries currently support React 18. We are actively evaluating the upgrade to React 19, which introduces significant changes, including making the [New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) mandatory.
|
||||
|
||||
## What this means for you
|
||||
|
||||
- **If you are already using the New JSX Transform:** You are not impacted by this change, regardless of your React version (17, 18, or when 19 becomes available for Backstage).
|
||||
- **If you are NOT using the New JSX Transform (likely if you're importing React like `import React from 'react'`):** You will need to adopt it before upgrading to React 19. This typically involves changing your imports, as the New JSX Transform doesn't require importing the entire React namespace to use JSX. This is recommended even on React 17 or 18, as it was a change introduced with React 17. Although a best practice since React 17, Backstage did not adopt this transform when it upgraded.
|
||||
|
||||
## Action Required
|
||||
|
||||
While upgrading to React 19 within Backstage is not yet officially supported, it's recommended to proactively adopt the New JSX Transform if you haven't already. This will ensure a smoother transition when React 19 support is introduced and improve compatibility with the current React ecosystem.
|
||||
|
||||
## Timeline
|
||||
|
||||
We are currently evaluating React 19 and will provide further guidance on the upgrade path and timelines soon. For now, you can prepare by adopting the New JSX Transform.
|
||||
|
||||
## Migration Process
|
||||
|
||||
### Updating React Imports
|
||||
|
||||
Find and replace all occurrences of `import * as React from 'react'` and `import React from 'react'` with named imports like:
|
||||
|
||||
```tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
```
|
||||
|
||||
If you must preserve the default React import for compatibility reasons, you can use:
|
||||
|
||||
```tsx
|
||||
import { default as React } from 'react';
|
||||
```
|
||||
|
||||
To streamline this process, consider using an automated codemod. Instructions are available in this [migration guide](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/jsx-migration-codemod.md).
|
||||
|
||||
### Updating Configuration Files
|
||||
|
||||
To ensure compatibility when using `@backstage/cli`, you must update your `tsconfig.json` to use the new JSX transforms.
|
||||
|
||||
#### TypeScript Configuration (TSConfig)
|
||||
|
||||
Update the `compilerOptions.jsx` setting in `tsconfig.json` to `react-jsx`. This file is typically located in the root directory.
|
||||
|
||||
```json filename="tsconfig.json"
|
||||
{
|
||||
"extends": "@backstage/cli/config/tsconfig.json",
|
||||
...
|
||||
"compilerOptions": {
|
||||
// highlight-remove-next-line
|
||||
"jsx": "react",
|
||||
// highlight-add-next-line
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the future, this setting will change to `preserve` once React 17 is fully deprecated.
|
||||
|
||||
##### Explanation of `compilerOptions.jsx` Values
|
||||
|
||||
- The `react` mode: This mode converts JSX into `React.createElement` calls, making it directly usable by React. The code doesn't require a separate JSX transformation step, and the output files will use the `.js` extension.
|
||||
|
||||
- The `react-jsx` mode: Introduced with React 17, this mode automatically handles the JSX transformation, allowing you to use JSX without needing to import `React` in each file.
|
||||
|
||||
- The `preserve` mode: This option leaves the JSX code untouched, embedding it directly into the output files. This is useful when you're planning to process the JSX with another tool like Babel later. The resulting files will use the `.jsx` extension to indicate the presence of JSX.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html)
|
||||
- [React 19 Upgrade Guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide)
|
||||
@@ -1,74 +1,126 @@
|
||||
---
|
||||
id: react18-migration
|
||||
title: Migrating to React 18
|
||||
description: Additional resources for the Material UI v5 migration guide specifically for Backstage
|
||||
description: A guide to migrating your project to React 18
|
||||
---
|
||||
|
||||
The Backstage core libraries and plugins are compatible with all versions of React from v16.8 to v18. This means that you can migrate projects at your own pace. We do however encourage you to do so sooner rather than later, both to keep up with the evolving ecosystem, but also because React 18 brings performance improvements, in particular in tests.
|
||||
:::info
|
||||
|
||||
This guide has been updated to include steps for removing support for React 16, as it is now deprecated.
|
||||
|
||||
:::
|
||||
|
||||
The Backstage core libraries and plugins are compatible with all versions of React from v17 to v18. This means that you can migrate projects at your own pace. We do however encourage you to do so sooner rather than later, both to keep up with the evolving ecosystem, but also because React 18 brings performance improvements, in particular in tests.
|
||||
|
||||
## Migration
|
||||
|
||||
_Before diving in, this is a heads-up that for large projects this can be a tricky migration due to the fact that it is hard to break down into a gradual migration. In practice the difficult part of this migration is switching to the new version of the `@testing-library/react` package for tests, since there is no overlapping support across major React versions, more on that later._
|
||||
|
||||
### Switching to React 18
|
||||
### Upgrading to React 18
|
||||
|
||||
To switch a project to React 18, there are generally three changes that need to be made.
|
||||
#### Backstage Instance
|
||||
|
||||
1. Update the resolutions in your root `package.json` to the new versions of `@types/react` and `@types/react-dom`:
|
||||
To migrate a Backstage instance to React 18, follow these steps:
|
||||
|
||||
```json title="package.json"
|
||||
"resolutions": {
|
||||
// highlight-remove-next-line
|
||||
"@types/react": "^17",
|
||||
// highlight-remove-next-line
|
||||
"@types/react-dom": "^17",
|
||||
// highlight-add-next-line
|
||||
"@types/react": "^18",
|
||||
// highlight-add-next-line
|
||||
"@types/react-dom": "^18",
|
||||
},
|
||||
```
|
||||
1. Modify the `resolutions` section in your root `package.json` to reference the latest versions of `@types/react` and `@types/react-dom`:
|
||||
|
||||
2. Update the `react` and `react-dom` dependencies in your `packages/app/package.json` to the new versions:
|
||||
```json title="package.json"
|
||||
"resolutions": {
|
||||
// highlight-remove-start
|
||||
"@types/react": "^17",
|
||||
"@types/react-dom": "^17",
|
||||
// highlight-remove-end
|
||||
// highlight-add-start
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
// highlight-add-end
|
||||
},
|
||||
```
|
||||
|
||||
```json title="packages/app/package.json"
|
||||
"dependencies": {
|
||||
...
|
||||
// highlight-remove-next-line
|
||||
"react": "^17.0.2",
|
||||
// highlight-remove-next-line
|
||||
"react-dom": "^17.0.2",
|
||||
// highlight-add-next-line
|
||||
"react": "^18.0.2",
|
||||
// highlight-add-next-line
|
||||
"react-dom": "^18.0.2",
|
||||
...
|
||||
},
|
||||
```
|
||||
2. Update the `react` and `react-dom` dependencies in `packages/app/package.json`:
|
||||
|
||||
3. Update `packages/app/src/index.tsx` to use the new `react-dom/client` API to render the app:
|
||||
```json title="packages/app/package.json"
|
||||
"dependencies": {
|
||||
...
|
||||
// highlight-remove-start
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
// highlight-remove-end
|
||||
// highlight-add-start
|
||||
"react": "^18.0.2",
|
||||
"react-dom": "^18.0.2",
|
||||
// highlight-add-end
|
||||
...
|
||||
},
|
||||
```
|
||||
|
||||
```tsx title="packages/app/src/index.tsx"
|
||||
import '@backstage/cli/asset-types';
|
||||
// highlight-remove-next-line
|
||||
import ReactDOM from 'react-dom';
|
||||
// highlight-add-next-line
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
3. Adjust `packages/app/src/index.tsx` to use the `react-dom/client` API for rendering:
|
||||
|
||||
// highlight-remove-next-line
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
// highlight-add-next-line
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
||||
```
|
||||
```tsx title="packages/app/src/index.tsx"
|
||||
import '@backstage/cli/asset-types';
|
||||
// highlight-remove-next-line
|
||||
import ReactDOM from 'react-dom';
|
||||
// highlight-add-next-line
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
Once these steps are done you should be able to run your app and see it working as before, except now using React 18.
|
||||
// highlight-remove-next-line
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
// highlight-add-next-line
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
||||
```
|
||||
|
||||
#### Backstage Frontend Plugin
|
||||
|
||||
1. Update the `devDependencies` and `peerDependencies` for `react`, `react-dom`, and `@types/react` in your plugin's `package.json`:
|
||||
|
||||
```json title="plugins/<plugin-name>/package.json"
|
||||
"devDependencies": {
|
||||
...
|
||||
// highlight-remove-start
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"react": "^16.13.1 || ^17.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0",
|
||||
// highlight-remove-end
|
||||
// highlight-add-start
|
||||
"@types/react": "^17.0.0 || ^18.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0",
|
||||
// highlight-add-end
|
||||
...
|
||||
},
|
||||
```
|
||||
|
||||
```json title="plugins/<plugin-name>/package.json"
|
||||
"peerDependencies": {
|
||||
...
|
||||
// highlight-remove-start
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"react": "^16.13.1 || ^17.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0",
|
||||
// highlight-remove-end
|
||||
// highlight-add-start
|
||||
"@types/react": "^17.0.0 || ^18.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0",
|
||||
// highlight-add-end
|
||||
...
|
||||
},
|
||||
```
|
||||
|
||||
After completing these updates, your application and plugins should function as before, now utilizing React 18.
|
||||
|
||||
:::note
|
||||
|
||||
Be sure to update your lockfile after modifying your `package.json` files.
|
||||
|
||||
:::
|
||||
|
||||
### TypeScript Errors
|
||||
|
||||
When upgrading to React 18 you are likely to see a fair number of TypeScript type errors. A summary of the breaking changes can be found in the [Pull Request that introduced them](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/56210). A codemod is also provided to help with the migration.
|
||||
|
||||
Run `yarn tsc:full` to asses the damage.
|
||||
Run `yarn tsc:full` to assess the damage.
|
||||
|
||||
The good news is that these errors can be fixed while still staying on React 17. If you have a large number of errors to fix you can address as few or many is you like at a time and merge them into your main branch **without** the version bumps from step 1. This lets you gradually migrate the types in your project while not yet fully moving over to React 18. Once all type breakages are fixed you can move on to the next step of migrating tests.
|
||||
|
||||
|
||||
@@ -563,6 +563,7 @@ export default {
|
||||
'tutorials/yarn-migration',
|
||||
'tutorials/migrate-to-mui5',
|
||||
'tutorials/auth-service-migration',
|
||||
'tutorials/jsx-transform-migration',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -249,13 +249,13 @@ function createConfigForRole(dir, role, extraConfig = {}) {
|
||||
'warn',
|
||||
{
|
||||
message:
|
||||
'React default imports are deprecated. Follow the x migration guide for details.',
|
||||
'React default imports are deprecated. Follow the https://backstage.io/docs/tutorials/jsx-transform-migration migration guide for details.',
|
||||
selector:
|
||||
"ImportDeclaration[source.value='react'][specifiers.0.type='ImportDefaultSpecifier']",
|
||||
},
|
||||
{
|
||||
message:
|
||||
'React default imports are deprecated. Follow the x migration guide for details. If you need a global type that collides with a React named export (such as `MouseEvent`), try using `globalThis.MouseHandler`.',
|
||||
'React default imports are deprecated. Follow the https://backstage.io/docs/tutorials/jsx-transform-migration migration guide for details. If you need a global type that collides with a React named export (such as `MouseEvent`), try using `globalThis.MouseHandler`.',
|
||||
selector:
|
||||
"ImportDeclaration[source.value='react'] :matches(ImportDefaultSpecifier, ImportNamespaceSpecifier)",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user