Skip to content

Commit 9ee52e9

Browse files
v-rakeshshJeevaniChinthalav-sharmachirSaanicaG
authored
feat(react 18): upgrade to react 18. (#7336)
#### Details This feature updates below packages. 1. react from v16 to v18. 2. react-dom from v16 to v18. 3. @types-react from v16 to v18. 4. @types-react-dom from v16 to v18. 5. @testing-library/react from v12 to v15. 6. @fluentui/react from v8.x.x to v8.118.1. 7. Removed react-helmet and added react-helmet-async. **1. Notable changes for react, react-dom:** **Motivation:** React 18 introduces a new root API which provides better ergonomics for managing roots. The new root API also enables the new concurrent renderer, which allows you to opt-into concurrent features. **In V16, we had below to render the component:** import { render } from 'react-dom'; const container = document.getElementById('app'); render(<App tab="home" />, container); - **In V18, we have below to render the component:** import { createRoot } from 'react-dom/client'; const container = document.getElementById('app'); const root = createRoot(container); // createRoot(container!) if you use TypeScript root.render(<App tab="home" />); **2. Notable changes for @types-react and @types-react-dom:** **Motivation:** The new types are safer and catch issues that used to be ignored by the type checker. The most notable change is that the children prop now needs to be listed explicitly when defining props - In old we have below WrappedComponent: React.ComponentType<P>, - In new we have below WrappedComponent: React.ComponentType<**React.PropsWithChildren<P>**>, **Approach for type changes:** So this Type changes are added using automation script https://github.com/eps1lon/types-react-codemod. This automation script is suggested in react18 migration document. - Added new package types-react-codemod. - After adding the package, executed yarn types-react-codemod preset-18 ./src in root, and then selected all option from the list of options. - This will transform all types of component type having child components to <React.PropsWithChildren<P>>. **3. Notable changes for @testing-library/react:** - Current version of @testing-library/react does not support react18, so from v13.x.x, react18 support is added. So updated to latest V15. For reference - https://github.com/testing-library/react-testing-library/releases/tag/v13.0.0 - Wrapped state updates/async operations under act. - Updated test cases with createRoot for createRootMock instead of render and renderMock. - **4. Notable changes for @fluentui/react from v8.x.x to v8.118.1** - Existing fluent ui version does not support react18, test cases were failing, hence after checking v8.118.1 documentation, it supports react and react-dom v18. Hence upadated. **5. Notable changes for react-helmet-async:** - Current react-helmet package throws error 'objects cannot be child, expected elements', for react18, Hence as alternative used react-helmet-async. For reference https://www.npmjs.com/package/react-helmet-async?activeTab=readme because react-helmet-async uses react18 as dependency. - Wrapped Helmet provider for root, as to pass context of react-helmet-async. - Created a variable to store data, and then this data was passed as JSX, instead of passing the data as it is. Because it will throw **"Objects cannot be used as react elements"**. **For example:** `export const GuidanceTitle = NamedFC<GuidanceTitleProps>('GuidanceTitle', ({ name }) => { const titleValue = `Guidance for ${name} - ${productName}`; return ( <> <Helmet> <title>{titleValue}</title> </Helmet> <h1>{name}</h1> </> ); });` **6. Along with above** - Made changes to mock helpers, because after react18 changes, the JSON structure of component was coming differently, so accordingly corrected the helpers, to get proper component name for snapshots. - Updated snapshots, because as we are using latest Fluent UI version, new props are introduced which can be seen in snapshots. - Refactored few test cases, which were wrong logically, like for example: using of mockReactComponents in global and inside test case using of useOriginalComponents to get the props using getMockComponentClassPropsForCall which was wrong logically is fixed to use any one approach. - Updated report package with react, react-dom v18 to keep in sync with AI web. ##### Context This PR includes all changes required for migration of AI web from react16 to react18. It includes test cases fixes. It includes lint issues fixes. <!-- Are there any parts that you've intentionally left out-of-scope for a later PR to handle? --> <!-- Were there any alternative approaches you considered? What tradeoffs did you consider? --> #### Pull request checklist <!-- If a checklist item is not applicable to this change, write "n/a" in the checkbox --> - [ ] Addresses an existing issue: #0000 - [x] Ran `yarn fastpass` - [x] Added/updated relevant unit test(s) (and ran `yarn test`) - [x] Verified code coverage for the changes made. Check coverage report at: `<rootDir>/test-results/unit/coverage` - [x] PR title *AND* final merge commit title both start with a semantic tag (`fix:`, `chore:`, `feat(feature-name):`, `refactor:`). See `CONTRIBUTING.md`. - [ ] (UI changes only) Added screenshots/GIFs to description above - [x] (UI changes only) Verified usability with NVDA/JAWS --------- Co-authored-by: Jeevani Chinthala <[email protected]> Co-authored-by: JeevaniChinthala <[email protected]> Co-authored-by: v-sharmachir <[email protected]> Co-authored-by: Chirag Sharma <[email protected]> Co-authored-by: Saanica Ghate <[email protected]> Co-authored-by: Saanica Ghate <[email protected]>
1 parent 1a26c8f commit 9ee52e9

File tree

88 files changed

+3073
-4773
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+3073
-4773
lines changed

package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,15 @@
7979
"@swc/core": "^1.3.107",
8080
"@swc/jest": "^0.2.36",
8181
"@testing-library/jest-dom": "^6.4.2",
82-
"@testing-library/react": "12.1.2",
82+
"@testing-library/react": "^15.0.5",
8383
"@types/chrome": "0.0.260",
8484
"@types/jest": "^29.5.11",
8585
"@types/jsdom": "^21.1.6",
8686
"@types/lodash": "^4.14.202",
8787
"@types/luxon": "^3.4.2",
8888
"@types/node": "^16.11.7",
89-
"@types/react": "^16.14.25",
90-
"@types/react-dom": "^16.9.15",
89+
"@types/react": "^18.3.1",
90+
"@types/react-dom": "^18.3.0",
9191
"@types/react-helmet": "^6.1.11",
9292
"@types/react-router-dom": "^5.3.3",
9393
"@types/serve-static": "^1.15.5",
@@ -151,7 +151,7 @@
151151
"webpack-node-externals": "^3.0.0"
152152
},
153153
"dependencies": {
154-
"@fluentui/react": "^8.96.1",
154+
"@fluentui/react": "^8.118.1",
155155
"@microsoft/applicationinsights-web": "^2.8.15",
156156
"@testing-library/user-event": "^14.5.2",
157157
"ajv": "^8.12.0",
@@ -160,9 +160,9 @@
160160
"idb-keyval": "^6.2.1",
161161
"lodash": "^4.17.21",
162162
"luxon": "^3.4.4",
163-
"react": "^16.14.0",
164-
"react-dom": "^16.14.0",
165-
"react-helmet": "^6.1.0",
163+
"react": "^18.3.1",
164+
"react-dom": "^18.3.1",
165+
"react-helmet-async": "^2.0.5",
166166
"react-resize-detector": "^9.1.1",
167167
"react-router-dom": "^6.21.3",
168168
"tabbable": "^6.2.0",

packages/report/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@
1818
"url": "https://github.com/Microsoft/accessibility-insights-web"
1919
},
2020
"dependencies": {
21-
"@fluentui/react": "^8.96.1",
21+
"@fluentui/react": "^8.118.1",
2222
"axe-core": "4.8.4",
2323
"classnames": "^2.5.1",
2424
"lodash": "^4.17.21",
2525
"luxon": "^3.4.4",
26-
"react": "^16.14.0",
27-
"react-dom": "^16.14.0",
28-
"react-helmet": "^6.1.0",
26+
"react": "^18.3.1",
27+
"react-dom": "^18.3.1",
28+
"react-helmet-async": "^2.0.5",
2929
"uuid": "^9.0.1"
3030
}
3131
}

packages/ui/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
"classnames": "^2.5.1",
2323
"lodash": "^4.17.21",
2424
"luxon": "^3.4.4",
25-
"react": "^16.14.0",
26-
"react-dom": "^16.14.0",
27-
"react-helmet": "^6.1.0",
25+
"react": "^18.3.1",
26+
"react-dom": "^18.3.1",
27+
"react-helmet-async": "^2.0.5",
2828
"react-resize-detector": "^9.1.1",
2929
"uuid": "^9.0.1"
3030
}

src/DetailsView/components/assessment-instance-table.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { AssessmentDefaultMessageGenerator } from 'assessments/assessment-defaul
1414
import { InstanceTableHeaderType, InstanceTableRow } from 'assessments/types/instance-table-data';
1515
import { InsightsCommandButton } from 'common/components/controls/insights-command-button';
1616
import { ManualTestStatus } from 'common/types/store-data/manual-test-status';
17-
import { has } from 'lodash';
17+
import { hasIn } from 'lodash';
1818
import * as React from 'react';
1919
import {
2020
AssessmentNavState,
@@ -137,7 +137,7 @@ export class AssessmentInstanceTable extends React.Component<AssessmentInstanceT
137137
private isAnyInstanceStatusUnknown(items: InstanceTableRow[], step: string): boolean {
138138
return items.some(
139139
item =>
140-
has(item.instance.testStepResults, step) &&
140+
hasIn(item.instance.testStepResults, step) &&
141141
item.instance.testStepResults[step].status === ManualTestStatus.UNKNOWN,
142142
);
143143
}

src/DetailsView/details-view-initializer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ import {
8181
TabStopsFailedCounterIncludingNoInstance,
8282
TabStopsFailedCounterInstancesOnly,
8383
} from 'DetailsView/tab-stops-failed-counter';
84-
import * as ReactDOM from 'react-dom';
84+
import * as ReactDOMClient from 'react-dom/client';
8585
import { ReportExportServiceProviderImpl } from 'report-export/report-export-service-provider-impl';
8686
import { AssessmentJsonExportGenerator } from 'reports/assessment-json-export-generator';
8787
import { AssessmentReportHtmlGenerator } from 'reports/assessment-report-html-generator';
@@ -735,7 +735,7 @@ if (tabId != null) {
735735
const renderer = new DetailsViewRenderer(
736736
deps,
737737
dom,
738-
ReactDOM.render,
738+
ReactDOMClient.createRoot,
739739
documentElementSetter,
740740
);
741741

@@ -751,7 +751,7 @@ if (tabId != null) {
751751
.catch(() => {
752752
const renderer = createNullifiedRenderer(
753753
document,
754-
ReactDOM.render,
754+
ReactDOMClient.createRoot,
755755
createDefaultLogger(),
756756
);
757757
renderer.render();
@@ -760,7 +760,7 @@ if (tabId != null) {
760760

761761
function createNullifiedRenderer(
762762
doc: Document,
763-
render: typeof ReactDOM.render,
763+
createRoot: typeof ReactDOMClient.createRoot,
764764
logger: Logger,
765765
): NoContentAvailableViewRenderer {
766766
// using an instance of an actual store (instead of a StoreProxy) so we can get the default state.
@@ -773,5 +773,5 @@ function createNullifiedRenderer(
773773
getNarrowModeThresholds: getNarrowModeThresholdsForWeb,
774774
};
775775

776-
return new NoContentAvailableViewRenderer(deps, doc, render, documentElementSetter);
776+
return new NoContentAvailableViewRenderer(deps, doc, createRoot, documentElementSetter);
777777
}
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33
import * as React from 'react';
4-
import * as ReactDOM from 'react-dom';
4+
import * as ReactDOMClient from 'react-dom/client';
55
import { Theme, ThemeDeps } from '../common/components/theme';
66
import { config } from '../common/configuration';
77
import { DocumentManipulator } from '../common/document-manipulator';
@@ -12,21 +12,20 @@ export class DetailsViewRenderer {
1212
constructor(
1313
private readonly deps: DetailsViewRendererDeps,
1414
private readonly dom: Document,
15-
private readonly renderer: typeof ReactDOM.render,
15+
private readonly createRoot: typeof ReactDOMClient.createRoot,
1616
private readonly documentManipulator: DocumentManipulator,
1717
) {}
1818

1919
public render(): void {
20-
const detailsViewContainer = this.dom.querySelector('#details-container');
20+
const detailsViewContainer = this.dom.querySelector('#details-container') as Element;
2121
const iconPath = '../' + config.getOption('icon128');
2222
this.documentManipulator.setShortcutIcon(iconPath);
23-
24-
this.renderer(
23+
const root = this.createRoot(detailsViewContainer);
24+
root.render(
2525
<>
2626
<Theme deps={this.deps} />
2727
<DetailsView deps={this.deps} />
2828
</>,
29-
detailsViewContainer,
3029
);
3130
}
3231
}

src/DetailsView/no-content-available-view-renderer.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,22 @@ import {
66
NoContentAvailableViewDeps,
77
} from 'DetailsView/components/no-content-available/no-content-available-view';
88
import * as React from 'react';
9-
import * as ReactDOM from 'react-dom';
9+
import * as ReactDOMClient from 'react-dom/client';
1010
import { config } from '../common/configuration';
1111

1212
export class NoContentAvailableViewRenderer {
1313
constructor(
1414
private readonly deps: NoContentAvailableViewDeps,
1515
private readonly dom: Document,
16-
private readonly renderer: typeof ReactDOM.render,
16+
private readonly createRoot: typeof ReactDOMClient.createRoot,
1717
private readonly documentManipulator: DocumentManipulator,
1818
) {}
1919

2020
public render(): void {
21-
const detailsViewContainer = this.dom.querySelector('#details-container');
21+
const detailsViewContainer = this.dom.querySelector('#details-container') as Element;
2222
const iconPath = '../' + config.getOption('icon128');
2323
this.documentManipulator.setShortcutIcon(iconPath);
24-
25-
this.renderer(<NoContentAvailableView deps={this.deps} />, detailsViewContainer);
24+
const root = this.createRoot(detailsViewContainer);
25+
root.render(<NoContentAvailableView deps={this.deps} />);
2626
}
2727
}

src/assessments/common/property-bag-column-renderer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ export function propertyBagColumnRenderer<TPropertyBag extends ColumnValueBag>(
5050
propertyMap: DictionaryStringTo<string>,
5151
) => {
5252
if (isEmpty(propertyMap)) {
53-
return <React.Fragment>{config.defaultValue}</React.Fragment>;
53+
const value: any = config.defaultValue;
54+
return <React.Fragment>{value}</React.Fragment>;
5455
}
5556

5657
return Object.keys(propertyMap).map(key => {

src/common/components/with-store-subscription.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type WithStoreSubscriptionDeps<T> = {
1414
};
1515

1616
export function withStoreSubscription<P extends WithStoreSubscriptionProps<S>, S>(
17-
WrappedComponent: React.ComponentType<P>,
17+
WrappedComponent: React.ComponentType<React.PropsWithChildren<P>>,
1818
): React.ComponentClass<Pick<P, Exclude<keyof P, keyof { storeState: S }>>, Partial<S>> & {
1919
displayName: string;
2020
} {

src/common/extensibility/react-extension-point.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ type ExtensionPoint<C> = {
2020
apply: (component: C) => Extension<C>;
2121
};
2222

23-
type ReactExtension<P> = Extension<React.FC<P>> & {
23+
type ReactExtension<P> = Extension<React.FC<React.PropsWithChildren<P>>> & {
2424
extensionType: 'reactComponent';
2525
};
2626

27-
type ReactExtensionPoint<P extends {}> = ExtensionPoint<React.FC<P>> & {
27+
type ReactExtensionPoint<P extends {}> = ExtensionPoint<React.FC<React.PropsWithChildren<P>>> & {
2828
extensionType: 'reactComponent';
29-
create: (component: React.FC<P>) => ReactExtension<P>;
30-
component: React.FC<P & { extensions: AnyExtension[] }>;
29+
create: (component: React.FC<React.PropsWithChildren<P>>) => ReactExtension<P>;
30+
component: React.FC<React.PropsWithChildren<P & { extensions: AnyExtension[] }>>;
3131
};
3232

3333
function isReactExtension(extension: Extension<any>): extension is ReactExtension<any> {
@@ -55,7 +55,7 @@ export function reactExtensionPoint<P extends {}>(
5555
return result;
5656
});
5757

58-
function create(extensionComponent: React.FC<P>): ReactExtension<P> {
58+
function create(extensionComponent: React.FC<React.PropsWithChildren<P>>): ReactExtension<P> {
5959
const Wrap = extensionComponent;
6060
const wrapComponent = NamedFC<P>(extensionPointKey, props => <Wrap {...props} />);
6161
wrapComponent.displayName = extensionPointKey;

0 commit comments

Comments
 (0)