Skip to content

feat(react-utilities): enhance useFirstMount hook to support React concurrent mode with useEffect for first mount tracking #34985

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/react-17-tests-v9/config/support/commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { realPress } from 'cypress-real-events/commands/realPress';

/**
* Press command fallback for Cypress 13 compatibility.
* The press command is available in Cypress 14+ but not in Cypress 13.
*/
Cypress.Commands.add('press', realPress);
2 changes: 2 additions & 0 deletions apps/react-17-tests-v9/config/support/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import 'cypress-real-events';
import './commands';
8 changes: 7 additions & 1 deletion apps/react-17-tests-v9/cypress-react-17.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ const specs = [
path.resolve('../../packages/react-components/react-tabster/src/**/*.cy.{tsx,ts}'),
...excludedSpecs,
];
const config = { ...baseConfig };
const config = {
...baseConfig,
component: {
...baseConfig.component,
supportFile: path.resolve(__dirname, 'config/support/component.js'),
Copy link
Contributor

Choose a reason for hiding this comment

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

hmm this overrides https://github.com/microsoft/fluentui/blob/master/scripts/cypress/src/support/component.js, which for now should be ok but if we add some substantial in future it will be missing here. can we resolve the situation to still use our base supportFile inclusion ?

},
};

config.component.specPattern = specs;
config.component.devServer.webpackConfig.resolve ??= {};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: enhance hook to support React concurrent mode using setEffect for first mount tracking",
"packageName": "@fluentui/react-utilities",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('DrawerFooter', () => {

cy.get('#drawer-body').scrollTo('center');
cy.get('#drawer-footer').within($el => {
cy.window().then(win => {
cy.window().should(win => {
const before = win.getComputedStyle($el[0], '::before');
const opacity = before.getPropertyValue('opacity');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ describe('Keyborg', () => {

it('should dispose keyborg instance on unmount', () => {
mount(<Example />);
cy.window().then(win => {
cy.window().should(win => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Fixed failing React 19 integration e2e tests by updating Cypress test patterns from promise-based assertions to chained assertions to handle React 19's timing changes in concurrent mode

just to clarify, these were failing all along mainly because of this ? or without the fix to useFirstMount it would still fail ?

// @ts-expect-error - Only way to definitively check if keyborg is disposed
expect(win.__keyborg).not.equals(undefined);
});
mount(<div>Unmounted</div>);
cy.window().then(win => {
cy.window().should(win => {
Copy link
Contributor

Choose a reason for hiding this comment

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

should we bring some lint rule to disable using then on cypress APIs to mitigate this ?

// @ts-expect-error - Only way to definitively check if keyborg is disposed
expect(win.__keyborg).equals(undefined);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'cypress-real-events';
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't this loaded via our baseconfig ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure, it's imported in the Tree component in the same package, but not for the FlatTree component. I'll double check if it's needed

import * as React from 'react';
import { mount as mountBase } from '@cypress/react';
import { FluentProvider } from '@fluentui/react-provider';
Expand Down Expand Up @@ -203,9 +204,9 @@ describe('FlatTree', () => {
</TreeTest>,
);
cy.focused().should('not.exist');
cy.document().realPress('Tab');
Copy link
Contributor Author

@dmytrokirpa dmytrokirpa Aug 8, 2025

Choose a reason for hiding this comment

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

realPress supports React 17 and 18, but not 19.
press is compatible with React 18 and 19.

So, I use press and provide a fallback for press in Cypress 13, since it is not available there.

cy.document().press('Tab');
cy.get('[data-testid="item1"]').should('be.focused');
cy.document().realPress('Tab');
cy.document().press('Tab');
cy.get('#action').should('be.focused');
});
describe('navigationMode="treegrid"', () => {
Expand Down Expand Up @@ -241,9 +242,9 @@ describe('FlatTree', () => {
</TreeTest>,
);
cy.focused().should('not.exist');
cy.document().realPress('Tab');
cy.document().press('Tab');
cy.get('[data-testid="item1"]').should('be.focused');
cy.document().realPress('Tab');
cy.document().press('Tab');
cy.get('#action').should('be.focused').realPress('{enter}');
cy.get('[data-testid="item1__item1"]').should('not.exist');
cy.get('#action').should('be.focused').realPress('Space');
Expand All @@ -252,23 +253,23 @@ describe('FlatTree', () => {
it('should focus on first item when pressing tab key', () => {
mount(<TreeTest />);
cy.focused().should('not.exist');
cy.document().realPress('Tab');
cy.document().press('Tab');
cy.get('[data-testid="item1"]').should('be.focused');
});
it('should focus out of tree when pressing tab key inside tree.', () => {
mount(<TreeTest />);
cy.focused().should('not.exist');
cy.document().realPress('Tab');
cy.document().press('Tab');
cy.get('[data-testid="item1"]').should('be.focused');
cy.focused().realPress('Tab');
cy.focused().press('Tab');
cy.focused().should('not.exist');
});
describe('Navigation', () => {
it('should move with Up/Down keys', () => {
mount(<TreeTest />);
cy.get('[data-testid="item1"]').focus().realPress('{downarrow}');
cy.get('[data-testid="item2"]').should('be.focused');
cy.focused().realPress('Tab').should('not.exist');
cy.focused().press('Tab').should('not.exist');
});
describe('navigationMode="treegrid"', () => {
it('should move with Up/Down keys', () => {
Expand Down Expand Up @@ -434,7 +435,7 @@ describe('FlatTree', () => {
defaultCheckedItems={['item1__item1']}
/>,
);
cy.window().then(win => {
cy.window().should(win => {
expect(win.console.error).to.be.called;
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,20 @@ import * as React from 'react';
/**
* @internal
* Checks if components was mounted the first time.
* Since concurrent mode will be released in the future this needs to be verified
* Currently (React 17) will always render the initial mount once
* https://codesandbox.io/s/heuristic-brook-s4w0q?file=/src/App.jsx
* https://codesandbox.io/s/holy-grass-8nieu?file=/src/App.jsx
* Supports React concurrent/strict mode by using `useEffect`
* to track the first mount instead of mutating refs during render.
*
* @example
* const isFirstMount = useFirstMount();
*/
export function useFirstMount(): boolean {
const isFirst = React.useRef(true);

if (isFirst.current) {
isFirst.current = false;
return true;
}
React.useEffect(() => {
if (isFirst.current) {
isFirst.current = false;
}
}, []);

return isFirst.current;
}