Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react 18): upgrade to react 18. #7336

Open
wants to merge 44 commits into
base: main
Choose a base branch
from

Conversation

v-rakeshsh
Copy link
Contributor

@v-rakeshsh v-rakeshsh commented May 15, 2024

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(, 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();

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

    ,

  • In new we have below
    WrappedComponent: React.ComponentType<React.PropsWithChildren

    >,

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

    >.

3. Notable changes for @testing-library/react:

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.

Pull request checklist

  • Addresses an existing issue: #0000
  • Ran yarn fastpass
  • Added/updated relevant unit test(s) (and ran yarn test)
  • Verified code coverage for the changes made. Check coverage report at: <rootDir>/test-results/unit/coverage
  • 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
  • (UI changes only) Verified usability with NVDA/JAWS

@v-rakeshsh v-rakeshsh requested a review from a team as a code owner May 15, 2024 11:26
@v-rakeshsh v-rakeshsh marked this pull request as draft May 15, 2024 11:26
Copy link
Contributor

@v-viyada v-viyada left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the details for each type of change we did in PR description. Overall PR looks good but provided few nit pick comments. Please check for other similar changes.

@v-viyada v-viyada marked this pull request as ready for review May 24, 2024 16:38
@v-viyada v-viyada changed the title V rakeshsh/react18 migration feat(react 18): upgrade to react 18. May 24, 2024
Copy link
Contributor

@madalynrose madalynrose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is a huge effort! Well done, team! I have some questions about changes, mostly in tests.


const renderMock: any = Mock.ofType<typeof createRoot>();
createRootMock
.setup(r => r(It.isAny()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this change in the code.

src/tests/unit/mock-helpers/mock-module-helpers.tsx Outdated Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The props snapshots need a snapshot name ("Dialog props") passed in:

        it.each(isOpenOptions)('with open %p', isOpen => {
            props.isOpen = isOpen;
            onlyIncludeHtmlService();
            const renderResult = render(<ExportDialog {...props} />);
            expectMockedComponentPropsToMatchSnapshots([Dialog], 'Dialog props');
            expect(renderResult.asFragment()).toMatchSnapshot();
        });

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test also needs to mock (Icon as any).type in order to not have the generic Memo in the snapshot

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change i.e mockReactComponents([(Icon as any).type]) also mockReactComponent((Icon as any).type, 'Icon') Snapshot is not changing generic Memo, it is still there in the snapshot. Even if we try to do mockReactComponent(Icon, 'Icon') it gives error as mock-Icon is not a mockable component. Please add a jest.mock call for this component before using this component in the test function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay! I figured out a solution for this. React.memo is the thing determining the name so we can mock that function and adjust the output:

jest.mock('react', () => {
    const original = jest.requireActual('react');
    return {
        ...original,
        memo: jest.fn().mockImplementation((component, compare) => {
            const elementType = original.memo(component, compare);
            if (elementType.type && elementType.type.render) {
                if (elementType.type.render.displayName) {
                    elementType.type.name = elementType.type.render.displayName;
                }
            }
            return elementType;
        }),
    };
});

We don't need to mock Icon.type anymore either.

act(() => {
getMockComponentCall(StartOverDialog, 3)[0].dismissDialog();
});
fireEvent.click(startOverMenuButton);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't need a fireEvent call after the dismissDialog call for the button to be focused. dismissDialog should send focus to the button.

@@ -119,10 +123,13 @@ describe('FailureInstancePanelControlTest', () => {
const props = createPropsWithType(CapturedInstanceActionType.CREATE);

const renderResult = render(<FailureInstancePanelControl {...props} />);
renderResult.rerender(<FailureInstancePanelControl {...props} />);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this rerender necessary?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay! I figured out a solution for this. React.memo is the thing determining the name so we can mock that function and adjust the output:

jest.mock('react', () => {
    const original = jest.requireActual('react');
    return {
        ...original,
        memo: jest.fn().mockImplementation((component, compare) => {
            const elementType = original.memo(component, compare);
            if (elementType.type && elementType.type.render) {
                if (elementType.type.render.displayName) {
                    elementType.type.name = elementType.type.render.displayName;
                }
            }
            return elementType;
        }),
    };
});

We don't need to mock Icon.type anymore either.

Comment on lines +313 to +318
const component = new TestDiagnosticViewToggle(props);
component.isFocused = true;
render(component.render());
expect(component.isFocused).toBeTruthy();
getMockComponentClassPropsForCall(VisualizationToggle).onBlur();
expect(component.isFocused).toBeFalsy();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of a way to do this without adding to or even using our test class:

Suggested change
const component = new TestDiagnosticViewToggle(props);
component.isFocused = true;
render(component.render());
expect(component.isFocused).toBeTruthy();
getMockComponentClassPropsForCall(VisualizationToggle).onBlur();
expect(component.isFocused).toBeFalsy();
const component = new DiagnosticViewToggle(props);
render(component.render());
component.componentDidMount();
const setState = jest.spyOn(component, 'setState');
getMockComponentClassPropsForCall(VisualizationToggle).onBlur();
expect(setState).toHaveBeenCalledWith({ isFocused: false });

@@ -122,6 +110,14 @@ describe('SaveAssessmentButton', () => {
Times.atLeastOnce(),
);
});
it('dialog is hidden (dismissed) when "got it" button is clicked', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is still failing. I dug into it and there's some thing bizarre going on, potentially because of Fluent or React. I added console.log calls after each place I expected Dialog.render to have been called again and accessed (Dialog as any).render.mock.calls and found that after the link click in the beforeEach block, there's a mysterious third Dialog.render call with empty props. I put that link click inside of an act(() =>) call and added more logging and found:

 console.log
    after link click inside of act [
      [
        {
          hidden: true,
          onDismiss: [Function (anonymous)],
          dialogContentProps: [Object],
          modalProps: [Object],
          children: [Array]
        },
        null
      ]
    ]

      at log (src/tests/unit/tests/DetailsView/components/save-assessment-button.test.tsx:95:29)

  console.error
    Warning: contextType was defined as an instance property on CustomizedPrimaryButton. Use a static property to define contextType instead.
        at CustomizedPrimaryButton
        at mock-StackItem
        at StackItem
        at div
        at div
        at ResultComponent (eval at _createMockFunction (C:\repos\accessibility-insights-web\node_modules\jest-mock\build\index.js:566:31), Stack:3:61)
        at span
        at div
        at div
        at DialogFooterBase (C:\repos\accessibility-insights-web\node_modules\@fluentui\react\lib-commonjs\components\src\components\Dialog\DialogFooter.base.tsx:12:5)
        at mockConstructor (C:\repos\accessibility-insights-web\node_modules\jest-mock\build\index.js:148:19)
        at div
        at div
        at div
        at DialogContentBase (C:\repos\accessibility-insights-web\node_modules\@fluentui\react\lib-commonjs\components\src\components\Dialog\DialogContent.base.tsx:27:5)
        at WithResponsiveMode (C:\repos\accessibility-insights-web\node_modules\@fluentui\react\lib-commonjs\utilities\src\utilities\decorators\withResponsiveMode.tsx:85:7)
        at C:\repos\accessibility-insights-web\node_modules\@fluentui\utilities\src\styled.tsx:99:26
        at div
        at div
        at C:\repos\accessibility-insights-web\node_modules\@fluentui\react\lib-commonjs\components\src\components\FocusTrapZone\FocusTrapZone.tsx:66:22
        at div
        at div
        at C:\repos\accessibility-insights-web\node_modules\@fluentui\react\lib-commonjs\components\src\components\Popup\Popup.tsx:149:24
        at div
        at FocusRectsProvider (C:\repos\accessibility-insights-web\node_modules\@fluentui\utilities\src\FocusRectsProvider.tsx:17:43)
        at C:\repos\accessibility-insights-web\node_modules\@fluentui\react\lib-commonjs\components\src\components\Fabric\Fabric.base.tsx:38:77
        at C:\repos\accessibility-insights-web\node_modules\@fluentui\utilities\src\styled.tsx:99:26
        at FocusRectsProvider (C:\repos\accessibility-insights-web\node_modules\@fluentui\utilities\src\FocusRectsProvider.tsx:17:43)
        at span
        at C:\repos\accessibility-insights-web\node_modules\@fluentui\react\lib-commonjs\components\src\components\Layer\Layer.base.tsx:50:45
        at C:\repos\accessibility-insights-web\node_modules\@fluentui\utilities\src\styled.tsx:99:26
        at C:\repos\accessibility-insights-web\node_modules\@fluentui\react\lib-commonjs\components\src\components\Modal\Modal.base.tsx:134:27
        at C:\repos\accessibility-insights-web\node_modules\@fluentui\utilities\src\styled.tsx:99:26
        at DialogBase (C:\repos\accessibility-insights-web\node_modules\@fluentui\react\lib-commonjs\components\src\components\Dialog\Dialog.base.tsx:42:5)
        at WithResponsiveMode (C:\repos\accessibility-insights-web\node_modules\@fluentui\react\lib-commonjs\utilities\src\utilities\decorators\withResponsiveMode.tsx:85:7)
        at mockConstructor (C:\repos\accessibility-insights-web\node_modules\jest-mock\build\index.js:148:19)
        at C:\repos\accessibility-insights-web\src\DetailsView\components\save-assessment-button.tsx:27:89

  console.log
    after link click outside of act [
      [
        {
          hidden: true,
          onDismiss: [Function (anonymous)],
          dialogContentProps: [Object],
          modalProps: [Object],
          children: [Array]
        },
        null
      ],
      [
        {
          hidden: false,
          onDismiss: [Function (anonymous)],
          dialogContentProps: [Object],
          modalProps: [Object],
          children: [Array]
        },
        null
      ],
      []
    ]

      at Object.log (src/tests/unit/tests/DetailsView/components/save-assessment-button.test.tsx:100:25)

  console.log
    after got it click [
      [
        {
          hidden: true,
          onDismiss: [Function (anonymous)],
          dialogContentProps: [Object],
          modalProps: [Object],
          children: [Array]
        },
        null
      ],
      [
        {
          hidden: false,
          onDismiss: [Function (anonymous)],
          dialogContentProps: [Object],
          modalProps: [Object],
          children: [Array]
        },
        null
      ],
      [],
      [
        {
          hidden: true,
          onDismiss: [Function (anonymous)],
          dialogContentProps: [Object],
          modalProps: [Object],
          children: [Array]
        },
        null
      ]
    ]

      at Object.log (src/tests/unit/tests/DetailsView/components/save-assessment-button.test.tsx:127:25)

which suggests that the additional Dialog.render call is potentially happening after the fireEvent when resolving useEffect hooks. but I have no idea where this extra third render call is coming from or why beyond that. any ideas?

Changing the expect call to look at the fourth render will cause the test to pass, but I want to understand where the third render is coming from.

issueFilingSettingsContainer.onPropertyUpdateCallback(payload);
act(() => {
issueFilingSettingsContainer.onPropertyUpdateCallback(payload);
});

expect(renderResult.asFragment()).toMatchSnapshot();
expectMockedComponentPropsToMatchSnapshots([Dialog]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expectMockedComponentPropsToMatchSnapshots([Dialog]);
expectMockedComponentPropsToMatchSnapshots([Dialog], 'Dialog props');

issueFilingSettingsContainer.onPropertyUpdateCallback(payload);
act(() => {
issueFilingSettingsContainer.onPropertyUpdateCallback(payload);
});

expect(renderResult.asFragment()).toMatchSnapshot();
expectMockedComponentPropsToMatchSnapshots([Dialog]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expectMockedComponentPropsToMatchSnapshots([Dialog]);
expectMockedComponentPropsToMatchSnapshots([Dialog], 'Dialog props');

issueFilingSettingsContainer.onSelectedServiceChange(payload);
act(() => {
issueFilingSettingsContainer.onSelectedServiceChange(payload);
});

expect(renderResult.asFragment()).toMatchSnapshot();
expectMockedComponentPropsToMatchSnapshots([Dialog]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expectMockedComponentPropsToMatchSnapshots([Dialog]);
expectMockedComponentPropsToMatchSnapshots([Dialog], 'Dialog props');

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't make a suggestion for the final call of expectMockedComponentPropsToMatchSnapshots([Dialog]); (in the 'componentDidUpdate %s' test) because there isn't edited code near enough to it, but that one also needs to be updated with snapshotName.

@@ -142,7 +142,7 @@ export class DiagnosticViewToggle extends React.Component<
}
};

private onBlurHandler = (): void => {
protected onBlurHandler = (): void => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't need this anymore with the suggested changes to the test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants