Skip to content

Commit

Permalink
feat(react 18): upgrade to react 18. (#7336)
Browse files Browse the repository at this point in the history
#### 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]>
  • Loading branch information
7 people authored Jun 14, 2024
1 parent 1a26c8f commit 9ee52e9
Show file tree
Hide file tree
Showing 88 changed files with 3,073 additions and 4,773 deletions.
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@
"@swc/core": "^1.3.107",
"@swc/jest": "^0.2.36",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "12.1.2",
"@testing-library/react": "^15.0.5",
"@types/chrome": "0.0.260",
"@types/jest": "^29.5.11",
"@types/jsdom": "^21.1.6",
"@types/lodash": "^4.14.202",
"@types/luxon": "^3.4.2",
"@types/node": "^16.11.7",
"@types/react": "^16.14.25",
"@types/react-dom": "^16.9.15",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@types/react-helmet": "^6.1.11",
"@types/react-router-dom": "^5.3.3",
"@types/serve-static": "^1.15.5",
Expand Down Expand Up @@ -151,7 +151,7 @@
"webpack-node-externals": "^3.0.0"
},
"dependencies": {
"@fluentui/react": "^8.96.1",
"@fluentui/react": "^8.118.1",
"@microsoft/applicationinsights-web": "^2.8.15",
"@testing-library/user-event": "^14.5.2",
"ajv": "^8.12.0",
Expand All @@ -160,9 +160,9 @@
"idb-keyval": "^6.2.1",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-helmet": "^6.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet-async": "^2.0.5",
"react-resize-detector": "^9.1.1",
"react-router-dom": "^6.21.3",
"tabbable": "^6.2.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/report/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@
"url": "https://github.com/Microsoft/accessibility-insights-web"
},
"dependencies": {
"@fluentui/react": "^8.96.1",
"@fluentui/react": "^8.118.1",
"axe-core": "4.8.4",
"classnames": "^2.5.1",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-helmet": "^6.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet-async": "^2.0.5",
"uuid": "^9.0.1"
}
}
6 changes: 3 additions & 3 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
"classnames": "^2.5.1",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-helmet": "^6.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet-async": "^2.0.5",
"react-resize-detector": "^9.1.1",
"uuid": "^9.0.1"
}
Expand Down
4 changes: 2 additions & 2 deletions src/DetailsView/components/assessment-instance-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { AssessmentDefaultMessageGenerator } from 'assessments/assessment-defaul
import { InstanceTableHeaderType, InstanceTableRow } from 'assessments/types/instance-table-data';
import { InsightsCommandButton } from 'common/components/controls/insights-command-button';
import { ManualTestStatus } from 'common/types/store-data/manual-test-status';
import { has } from 'lodash';
import { hasIn } from 'lodash';
import * as React from 'react';
import {
AssessmentNavState,
Expand Down Expand Up @@ -137,7 +137,7 @@ export class AssessmentInstanceTable extends React.Component<AssessmentInstanceT
private isAnyInstanceStatusUnknown(items: InstanceTableRow[], step: string): boolean {
return items.some(
item =>
has(item.instance.testStepResults, step) &&
hasIn(item.instance.testStepResults, step) &&
item.instance.testStepResults[step].status === ManualTestStatus.UNKNOWN,
);
}
Expand Down
10 changes: 5 additions & 5 deletions src/DetailsView/details-view-initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ import {
TabStopsFailedCounterIncludingNoInstance,
TabStopsFailedCounterInstancesOnly,
} from 'DetailsView/tab-stops-failed-counter';
import * as ReactDOM from 'react-dom';
import * as ReactDOMClient from 'react-dom/client';
import { ReportExportServiceProviderImpl } from 'report-export/report-export-service-provider-impl';
import { AssessmentJsonExportGenerator } from 'reports/assessment-json-export-generator';
import { AssessmentReportHtmlGenerator } from 'reports/assessment-report-html-generator';
Expand Down Expand Up @@ -735,7 +735,7 @@ if (tabId != null) {
const renderer = new DetailsViewRenderer(
deps,
dom,
ReactDOM.render,
ReactDOMClient.createRoot,
documentElementSetter,
);

Expand All @@ -751,7 +751,7 @@ if (tabId != null) {
.catch(() => {
const renderer = createNullifiedRenderer(
document,
ReactDOM.render,
ReactDOMClient.createRoot,
createDefaultLogger(),
);
renderer.render();
Expand All @@ -760,7 +760,7 @@ if (tabId != null) {

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

return new NoContentAvailableViewRenderer(deps, doc, render, documentElementSetter);
return new NoContentAvailableViewRenderer(deps, doc, createRoot, documentElementSetter);
}
11 changes: 5 additions & 6 deletions src/DetailsView/details-view-renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as ReactDOMClient from 'react-dom/client';
import { Theme, ThemeDeps } from '../common/components/theme';
import { config } from '../common/configuration';
import { DocumentManipulator } from '../common/document-manipulator';
Expand All @@ -12,21 +12,20 @@ export class DetailsViewRenderer {
constructor(
private readonly deps: DetailsViewRendererDeps,
private readonly dom: Document,
private readonly renderer: typeof ReactDOM.render,
private readonly createRoot: typeof ReactDOMClient.createRoot,
private readonly documentManipulator: DocumentManipulator,
) {}

public render(): void {
const detailsViewContainer = this.dom.querySelector('#details-container');
const detailsViewContainer = this.dom.querySelector('#details-container') as Element;
const iconPath = '../' + config.getOption('icon128');
this.documentManipulator.setShortcutIcon(iconPath);

this.renderer(
const root = this.createRoot(detailsViewContainer);
root.render(
<>
<Theme deps={this.deps} />
<DetailsView deps={this.deps} />
</>,
detailsViewContainer,
);
}
}
10 changes: 5 additions & 5 deletions src/DetailsView/no-content-available-view-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ import {
NoContentAvailableViewDeps,
} from 'DetailsView/components/no-content-available/no-content-available-view';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as ReactDOMClient from 'react-dom/client';
import { config } from '../common/configuration';

export class NoContentAvailableViewRenderer {
constructor(
private readonly deps: NoContentAvailableViewDeps,
private readonly dom: Document,
private readonly renderer: typeof ReactDOM.render,
private readonly createRoot: typeof ReactDOMClient.createRoot,
private readonly documentManipulator: DocumentManipulator,
) {}

public render(): void {
const detailsViewContainer = this.dom.querySelector('#details-container');
const detailsViewContainer = this.dom.querySelector('#details-container') as Element;
const iconPath = '../' + config.getOption('icon128');
this.documentManipulator.setShortcutIcon(iconPath);

this.renderer(<NoContentAvailableView deps={this.deps} />, detailsViewContainer);
const root = this.createRoot(detailsViewContainer);
root.render(<NoContentAvailableView deps={this.deps} />);
}
}
3 changes: 2 additions & 1 deletion src/assessments/common/property-bag-column-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export function propertyBagColumnRenderer<TPropertyBag extends ColumnValueBag>(
propertyMap: DictionaryStringTo<string>,
) => {
if (isEmpty(propertyMap)) {
return <React.Fragment>{config.defaultValue}</React.Fragment>;
const value: any = config.defaultValue;
return <React.Fragment>{value}</React.Fragment>;
}

return Object.keys(propertyMap).map(key => {
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/with-store-subscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type WithStoreSubscriptionDeps<T> = {
};

export function withStoreSubscription<P extends WithStoreSubscriptionProps<S>, S>(
WrappedComponent: React.ComponentType<P>,
WrappedComponent: React.ComponentType<React.PropsWithChildren<P>>,
): React.ComponentClass<Pick<P, Exclude<keyof P, keyof { storeState: S }>>, Partial<S>> & {
displayName: string;
} {
Expand Down
10 changes: 5 additions & 5 deletions src/common/extensibility/react-extension-point.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ type ExtensionPoint<C> = {
apply: (component: C) => Extension<C>;
};

type ReactExtension<P> = Extension<React.FC<P>> & {
type ReactExtension<P> = Extension<React.FC<React.PropsWithChildren<P>>> & {
extensionType: 'reactComponent';
};

type ReactExtensionPoint<P extends {}> = ExtensionPoint<React.FC<P>> & {
type ReactExtensionPoint<P extends {}> = ExtensionPoint<React.FC<React.PropsWithChildren<P>>> & {
extensionType: 'reactComponent';
create: (component: React.FC<P>) => ReactExtension<P>;
component: React.FC<P & { extensions: AnyExtension[] }>;
create: (component: React.FC<React.PropsWithChildren<P>>) => ReactExtension<P>;
component: React.FC<React.PropsWithChildren<P & { extensions: AnyExtension[] }>>;
};

function isReactExtension(extension: Extension<any>): extension is ReactExtension<any> {
Expand Down Expand Up @@ -55,7 +55,7 @@ export function reactExtensionPoint<P extends {}>(
return result;
});

function create(extensionComponent: React.FC<P>): ReactExtension<P> {
function create(extensionComponent: React.FC<React.PropsWithChildren<P>>): ReactExtension<P> {
const Wrap = extensionComponent;
const wrapComponent = NamedFC<P>(extensionPointKey, props => <Wrap {...props} />);
wrapComponent.displayName = extensionPointKey;
Expand Down
6 changes: 4 additions & 2 deletions src/common/react/named-fc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// Licensed under the MIT License.
import * as React from 'react';

export type ReactFCWithDisplayName<P = {}> = React.FC<P> & { displayName: string };
export type ReactFCWithDisplayName<P = {}> = React.FC<React.PropsWithChildren<P>> & {
displayName: string;
};

export function NamedFC<P = {}>(
displayName: string,
component: React.FC<P>,
component: React.FC<React.PropsWithChildren<P>>,
): ReactFCWithDisplayName<P> {
component.displayName = displayName;

Expand Down
2 changes: 1 addition & 1 deletion src/common/types/link-component-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
// Licensed under the MIT License.
import { ILinkProps } from '@fluentui/react';

export type LinkComponentType = React.FC<ILinkProps>;
export type LinkComponentType = React.FC<React.PropsWithChildren<ILinkProps>>;
6 changes: 3 additions & 3 deletions src/debug-tools/initializer/debug-tools-init.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { defaultDateFormatter } from 'debug-tools/components/telemetry-viewer/te
import { TelemetryListener } from 'debug-tools/controllers/telemetry-listener';
import { DebugToolsNavStore } from 'debug-tools/stores/debug-tools-nav-store';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import UAParser from 'ua-parser-js';

export const initializeDebugTools = () => {
Expand Down Expand Up @@ -108,8 +108,8 @@ const createStoreProxies = (storeUpdateMessageHub: StoreUpdateMessageHub) => {

const render = (deps: DebugToolsViewDeps) => {
const container = document.querySelector('#debug-tools-container');

ReactDOM.render(<DebugToolsView deps={deps} />, container);
const root = createRoot(container);
root.render(<DebugToolsView deps={deps} />);
};

initializeDebugTools();
9 changes: 4 additions & 5 deletions src/injected/dialog-renderer-impl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { IssueFilingUrlStringUtils } from 'issue-filing/common/issue-filing-url-
import { PlainTextFormatter } from 'issue-filing/common/markup/plain-text-formatter';
import { AxeResultToIssueFilingDataConverter } from 'issue-filing/rule-result-to-issue-filing-data';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as ReactDOMClient from 'react-dom/client';
import { Target } from 'scanner/iruleresults';
import { DictionaryStringTo } from 'types/common-types';
import { rootContainerId } from './constants';
Expand All @@ -43,7 +43,7 @@ export class DialogRendererImpl implements DialogRenderer {

constructor(
private readonly dom: Document,
private readonly renderer: typeof ReactDOM.render,
private readonly createRoot: typeof ReactDOMClient.createRoot,
private readonly frameMessenger: SingleFrameMessenger,
private readonly htmlElementUtils: HTMLElementUtils,
private readonly windowUtils: WindowUtils,
Expand Down Expand Up @@ -101,8 +101,8 @@ export class DialogRendererImpl implements DialogRenderer {
userConfigMessageCreator: mainWindowContext.getUserConfigMessageCreator(),
LinkComponent: NewTabLink,
};

this.renderer(
const root = this.createRoot(dialogContainer);
root.render(
<LayeredDetailsDialogComponent
deps={deps}
failedRules={failedRules}
Expand All @@ -114,7 +114,6 @@ export class DialogRendererImpl implements DialogRenderer {
devToolsShortcut={getPlatform(this.windowUtils).devToolsShortcut}
devToolActionMessageCreator={mainWindowContext.getDevToolActionMessageCreator()}
/>,
dialogContainer,
);
return null;
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/injected/visualization/issues-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NavigatorUtils } from 'common/navigator-utils';
import { HtmlElementAxeResults } from 'common/types/store-data/visualization-scan-result-data';
import { DialogRendererImpl } from 'injected/dialog-renderer-impl';
import { SingleFrameMessenger } from 'injected/frameCommunicators/single-frame-messenger';
import * as ReactDOM from 'react-dom';
import * as ReactDOMClient from 'react-dom/client';

import { BrowserAdapter } from '../../common/browser-adapters/browser-adapter';
import { HTMLElementUtils } from '../../common/html-element-utils';
Expand All @@ -31,7 +31,7 @@ export class IssuesFormatter implements Formatter {
) {
this.dialogRenderer = new DialogRendererImpl(
document,
ReactDOM.render,
ReactDOMClient.createRoot,
frameMessenger,
htmlElementUtils,
windowUtils,
Expand Down
2 changes: 1 addition & 1 deletion src/popup/components/diagnostic-view-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class DiagnosticViewToggle extends React.Component<

// Must be consistent with internal react naming for same property to work
// tslint:disable-next-line: variable-name
private _isMounted: boolean;
protected _isMounted: boolean;

constructor(props: DiagnosticViewToggleProps) {
super(props);
Expand Down
11 changes: 5 additions & 6 deletions src/popup/incompatible-browser-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
// Licensed under the MIT License.
import { title } from 'content/strings/application';
import * as React from 'react';
import ReactDOM from 'react-dom';
import * as ReactDOMClient from 'react-dom/client';

import { NewTabLink } from '../common/components/new-tab-link';
import { Header } from './components/header';

export class IncompatibleBrowserRenderer {
constructor(
private readonly renderer: typeof ReactDOM.render,
private readonly createRoot: typeof ReactDOMClient.createRoot,
private readonly dom: Document,
) {}

public render(): void {
const container = this.dom.querySelector('#popup-container');

this.renderer(
const container = this.dom.querySelector('#popup-container') as Element;
const root = this.createRoot(container);
root.render(
<>
<div className="ms-Fabric unsupported-browser-info-panel">
<Header title={title} />
Expand All @@ -38,7 +38,6 @@ export class IncompatibleBrowserRenderer {
</div>
</div>
</>,
container,
);
}
}
Loading

0 comments on commit 9ee52e9

Please sign in to comment.