diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.test.js b/packages/mui-material/src/Autocomplete/Autocomplete.test.js
index 6b857e753a9e5a..26b9f892d58016 100644
--- a/packages/mui-material/src/Autocomplete/Autocomplete.test.js
+++ b/packages/mui-material/src/Autocomplete/Autocomplete.test.js
@@ -1384,6 +1384,11 @@ describe('', () => {
}).toWarnDev([
'returns duplicated headers',
!strictModeDoubleLoggingSupressed && 'returns duplicated headers',
+ // React 18 Strict Effects run mount effects twice which lead to a cascading update
+ React.version.startsWith('18') && 'returns duplicated headers',
+ React.version.startsWith('18') &&
+ !strictModeDoubleLoggingSupressed &&
+ 'returns duplicated headers',
]);
const options = screen.getAllByRole('option').map((el) => el.textContent);
expect(options).to.have.length(7);
diff --git a/packages/mui-material/src/useMediaQuery/useMediaQuery.test.js b/packages/mui-material/src/useMediaQuery/useMediaQuery.test.js
index 082f4ea33dc847..0e9e12a5776639 100644
--- a/packages/mui-material/src/useMediaQuery/useMediaQuery.test.js
+++ b/packages/mui-material/src/useMediaQuery/useMediaQuery.test.js
@@ -13,8 +13,6 @@ import mediaQuery from 'css-mediaquery';
import { expect } from 'chai';
import { stub } from 'sinon';
-const usesUseSyncExternalStore = React.useSyncExternalStore !== undefined;
-
function createMatchMedia(width, ref) {
const listeners = [];
return (query) => {
@@ -22,7 +20,6 @@ function createMatchMedia(width, ref) {
matches: mediaQuery.match(query, {
width,
}),
- // Mocking matchMedia in Safari < 14 where MediaQueryList doesn't inherit from EventTarget
addListener: (listener) => {
listeners.push(listener);
},
@@ -120,7 +117,7 @@ describe('useMediaQuery', () => {
render();
expect(screen.getByTestId('matches').textContent).to.equal('false');
- expect(getRenderCountRef.current()).to.equal(usesUseSyncExternalStore ? 1 : 2);
+ expect(getRenderCountRef.current()).to.equal(2);
});
});
@@ -160,10 +157,10 @@ describe('useMediaQuery', () => {
render();
expect(screen.getByTestId('matches').textContent).to.equal('false');
- expect(getRenderCountRef.current()).to.equal(usesUseSyncExternalStore ? 1 : 2);
+ expect(getRenderCountRef.current()).to.equal(2);
});
- it('should render once if the default value does not match the expectation but `noSsr` is enabled', () => {
+ it('should render once if the default value does not match the expectation', () => {
const getRenderCountRef = React.createRef();
const Test = () => {
const matches = useMediaQuery('(min-width:2000px)', {
@@ -200,13 +197,13 @@ describe('useMediaQuery', () => {
const { unmount } = render();
expect(screen.getByTestId('matches').textContent).to.equal('false');
- expect(getRenderCountRef.current()).to.equal(usesUseSyncExternalStore ? 1 : 2);
+ expect(getRenderCountRef.current()).to.equal(2);
unmount();
render();
expect(screen.getByTestId('matches').textContent).to.equal('false');
- expect(getRenderCountRef.current()).to.equal(usesUseSyncExternalStore ? 1 : 2);
+ expect(getRenderCountRef.current()).to.equal(2);
});
it('should be able to change the query dynamically', () => {
@@ -228,10 +225,10 @@ describe('useMediaQuery', () => {
const { setProps } = render();
expect(screen.getByTestId('matches').textContent).to.equal('false');
- expect(getRenderCountRef.current()).to.equal(usesUseSyncExternalStore ? 1 : 2);
+ expect(getRenderCountRef.current()).to.equal(2);
setProps({ query: '(min-width:100px)' });
expect(screen.getByTestId('matches').textContent).to.equal('true');
- expect(getRenderCountRef.current()).to.equal(usesUseSyncExternalStore ? 2 : 4);
+ expect(getRenderCountRef.current()).to.equal(4);
});
it('should observe the media query', () => {
diff --git a/packages/mui-material/src/useMediaQuery/useMediaQuery.ts b/packages/mui-material/src/useMediaQuery/useMediaQuery.ts
index bd77ff672f70cb..60038013f960a7 100644
--- a/packages/mui-material/src/useMediaQuery/useMediaQuery.ts
+++ b/packages/mui-material/src/useMediaQuery/useMediaQuery.ts
@@ -26,24 +26,42 @@ export type MuiMediaQueryListListener = (event: MuiMediaQueryListEvent) => void;
export interface Options {
defaultMatches?: boolean;
matchMedia?: typeof window.matchMedia;
- /**
- * This option is kept for backwards compatibility and has no longer any effect.
- * It's previous behavior is now handled automatically.
- */
- // TODO: Deprecate for v6
noSsr?: boolean;
ssrMatchMedia?: (query: string) => { matches: boolean };
}
-function useMediaQueryOld(
- query: string,
- defaultMatches: boolean,
- matchMedia: typeof window.matchMedia | null,
- ssrMatchMedia: ((query: string) => { matches: boolean }) | null,
- noSsr: boolean | undefined,
+export default function useMediaQuery(
+ queryInput: string | ((theme: Theme) => string),
+ options: Options = {},
): boolean {
+ const theme = useTheme();
+ // Wait for jsdom to support the match media feature.
+ // All the browsers MUI support have this built-in.
+ // This defensive check is here for simplicity.
+ // Most of the time, the match media logic isn't central to people tests.
const supportMatchMedia =
typeof window !== 'undefined' && typeof window.matchMedia !== 'undefined';
+ const {
+ defaultMatches = false,
+ matchMedia = supportMatchMedia ? window.matchMedia : null,
+ noSsr = false,
+ ssrMatchMedia = null,
+ } = getThemeProps({ name: 'MuiUseMediaQuery', props: options, theme });
+
+ if (process.env.NODE_ENV !== 'production') {
+ if (typeof queryInput === 'function' && theme === null) {
+ console.error(
+ [
+ 'MUI: The `query` argument provided is invalid.',
+ 'You are providing a function without a theme in the context.',
+ 'One of the parent elements needs to use a ThemeProvider.',
+ ].join('\n'),
+ );
+ }
+ }
+
+ let query = typeof queryInput === 'function' ? queryInput(theme) : queryInput;
+ query = query.replace(/^@media( ?)/m, '');
const [match, setMatch] = React.useState(() => {
if (noSsr && supportMatchMedia) {
@@ -75,7 +93,6 @@ function useMediaQueryOld(
}
};
updateMatch();
- // TODO: Use `addEventListener` once support for Safari < 14 is dropped
queryList.addListener(updateMatch);
return () => {
active = false;
@@ -83,90 +100,6 @@ function useMediaQueryOld(
};
}, [query, matchMedia, supportMatchMedia]);
- return match;
-}
-
-function useMediaQueryNew(
- query: string,
- defaultMatches: boolean,
- matchMedia: typeof window.matchMedia | null,
- ssrMatchMedia: ((query: string) => { matches: boolean }) | null,
-): boolean {
- const getDefaultSnapshot = React.useCallback(() => defaultMatches, [defaultMatches]);
- const getServerSnapshot = React.useMemo(() => {
- if (ssrMatchMedia !== null) {
- const { matches } = ssrMatchMedia(query);
- return () => matches;
- }
- return getDefaultSnapshot;
- }, [getDefaultSnapshot, query, ssrMatchMedia]);
- const [getSnapshot, subscribe] = React.useMemo(() => {
- if (matchMedia === null) {
- return [getDefaultSnapshot, () => () => {}];
- }
-
- const mediaQueryList = matchMedia(query);
-
- return [
- () => mediaQueryList.matches,
- (notify: () => void) => {
- // TODO: Use `addEventListener` once support for Safari < 14 is dropped
- mediaQueryList.addListener(notify);
- return () => {
- mediaQueryList.removeListener(notify);
- };
- },
- ];
- }, [getDefaultSnapshot, matchMedia, query]);
- const match = (React as any).useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
-
- return match;
-}
-
-export default function useMediaQuery(
- queryInput: string | ((theme: Theme) => string),
- options: Options = {},
-): boolean {
- const theme = useTheme();
- // Wait for jsdom to support the match media feature.
- // All the browsers MUI support have this built-in.
- // This defensive check is here for simplicity.
- // Most of the time, the match media logic isn't central to people tests.
- const supportMatchMedia =
- typeof window !== 'undefined' && typeof window.matchMedia !== 'undefined';
- const {
- defaultMatches = false,
- matchMedia = supportMatchMedia ? window.matchMedia : null,
- ssrMatchMedia = null,
- noSsr,
- } = getThemeProps({ name: 'MuiUseMediaQuery', props: options, theme });
-
- if (process.env.NODE_ENV !== 'production') {
- if (typeof queryInput === 'function' && theme === null) {
- console.error(
- [
- 'MUI: The `query` argument provided is invalid.',
- 'You are providing a function without a theme in the context.',
- 'One of the parent elements needs to use a ThemeProvider.',
- ].join('\n'),
- );
- }
- }
-
- let query = typeof queryInput === 'function' ? queryInput(theme) : queryInput;
- query = query.replace(/^@media( ?)/m, '');
-
- // TODO: Drop `useMediaQueryOld` and use `use-sync-external-store` shim in `useMediaQueryNew` once the package is stable
- const useMediaQueryImplementation =
- (React as any).useSyncExternalStore !== undefined ? useMediaQueryNew : useMediaQueryOld;
- const match = useMediaQueryImplementation(
- query,
- defaultMatches,
- matchMedia,
- ssrMatchMedia,
- noSsr,
- );
-
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useDebugValue({ query, match });
diff --git a/packages/mui-utils/src/useId.test.js b/packages/mui-utils/src/useId.test.js
index a69bd9b5228f75..66f22745cb5ee3 100644
--- a/packages/mui-utils/src/useId.test.js
+++ b/packages/mui-utils/src/useId.test.js
@@ -1,97 +1,36 @@
import * as React from 'react';
+import PropTypes from 'prop-types';
import { expect } from 'chai';
-import { createRenderer, screen } from 'test/utils';
+import { createRenderer } from 'test/utils';
import useId from './useId';
+const TestComponent = ({ id: idProp }) => {
+ const id = useId(idProp);
+ return {id};
+};
+
+TestComponent.propTypes = {
+ id: PropTypes.string,
+};
+
describe('useId', () => {
- const { render, renderToString } = createRenderer();
+ const { render } = createRenderer();
it('returns the provided ID', () => {
- const TestComponent = ({ id: idProp }) => {
- const id = useId(idProp);
- return ;
- };
- const { hydrate } = renderToString();
- const { setProps } = hydrate();
+ const { getByText, setProps } = render();
- expect(screen.getByTestId('target')).to.have.property('id', 'some-id');
+ expect(getByText('some-id')).not.to.equal(null);
setProps({ id: 'another-id' });
-
- expect(screen.getByTestId('target')).to.have.property('id', 'another-id');
+ expect(getByText('another-id')).not.to.equal(null);
});
it("generates an ID if one isn't provided", () => {
- const TestComponent = ({ id: idProp }) => {
- const id = useId(idProp);
- return ;
- };
- const { hydrate } = renderToString();
- const { setProps } = hydrate();
+ const { getByText, setProps } = render();
- expect(screen.getByTestId('target').id).not.to.equal('');
+ expect(getByText(/^mui-[0-9]+$/)).not.to.equal(null);
setProps({ id: 'another-id' });
- expect(screen.getByTestId('target')).to.have.property('id', 'another-id');
- });
-
- it('can be suffixed', () => {
- function Widget() {
- const id = useId();
- const labelId = `${id}-label`;
-
- return (
-
-
-
- Label
-
-
- );
- }
- render();
-
- expect(screen.getByTestId('labelable')).to.have.attr(
- 'aria-labelledby',
- screen.getByTestId('label').id,
- );
- });
-
- it('can be used in in IDREF attributes', () => {
- function Widget() {
- const labelPartA = useId();
- const labelPartB = useId();
-
- return (
-
-
-
- A
-
-
- B
-
-
- );
- }
- render();
-
- expect(screen.getByTestId('labelable')).to.have.attr(
- 'aria-labelledby',
- `${screen.getByTestId('labelA').id} ${screen.getByTestId('labelB').id}`,
- );
- });
-
- it('provides an ID on server in React 18', function test() {
- if (React.useId === undefined) {
- this.skip();
- }
- const TestComponent = () => {
- const id = useId();
- return ;
- };
- renderToString();
-
- expect(screen.getByTestId('target').id).not.to.equal('');
+ expect(getByText('another-id')).not.to.equal(null);
});
});
diff --git a/packages/mui-utils/src/useId.ts b/packages/mui-utils/src/useId.ts
index 95ca13a8604fc4..a756b4df1fb5b5 100644
--- a/packages/mui-utils/src/useId.ts
+++ b/packages/mui-utils/src/useId.ts
@@ -1,6 +1,6 @@
import * as React from 'react';
-function useRandomId(idOverride?: string): string | undefined {
+export default function useId(idOverride?: string): string | undefined {
const [defaultId, setDefaultId] = React.useState(idOverride);
const id = idOverride || defaultId;
React.useEffect(() => {
@@ -13,19 +13,3 @@ function useRandomId(idOverride?: string): string | undefined {
}, [defaultId]);
return id;
}
-
-/**
- *
- * @example
- * @param idOverride
- * @returns {string}
- */
-export default function useReactId(idOverride?: string): string | undefined {
- // TODO: Remove `React as any` once `useId` is part of stable types.
- if ((React as any).useId !== undefined) {
- const reactId = (React as any).useId();
- return idOverride ?? reactId;
- }
- // eslint-disable-next-line react-hooks/rules-of-hooks -- `React.useId` is invariant at runtime.
- return useRandomId(idOverride);
-}
diff --git a/scripts/use-react-dist-tag.js b/scripts/use-react-dist-tag.js
index 0a5c0d94cda847..405d4de4831de0 100644
--- a/scripts/use-react-dist-tag.js
+++ b/scripts/use-react-dist-tag.js
@@ -16,14 +16,7 @@ const { promisify } = require('util');
const exec = promisify(childProcess.exec);
// packages published from the react monorepo using the same version
-const reactPackageNames = [
- 'react',
- 'react-dom',
- 'react-is',
- 'react-test-renderer',
- 'scheduler',
- 'use-sync-external-store',
-];
+const reactPackageNames = ['react', 'react-dom', 'react-is', 'react-test-renderer', 'scheduler'];
async function main(options) {
const { distTag } = options;