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

add shim for useSyncExternalStore #1956

Merged
merged 11 commits into from
Mar 27, 2024
1 change: 1 addition & 0 deletions packages/itwinui-react/src/core/utils/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './useIsomorphicLayoutEffect.js';
export * from './useIsClient.js';
export * from './useId.js';
export * from './useControlledState.js';
export * from './useSyncExternalStore.js';
43 changes: 15 additions & 28 deletions packages/itwinui-react/src/core/utils/hooks/useMediaQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,23 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
import { getWindow } from '../functions/index.js';
import { useLayoutEffect } from './useIsomorphicLayoutEffect.js';
import { useSyncExternalStore } from './useSyncExternalStore.js';

export const useMediaQuery = (queryString: string) => {
const [matches, setMatches] = React.useState<boolean>();

useLayoutEffect(() => {
const mediaQueryList = getWindow()?.matchMedia?.(queryString);
const handleChange = ({ matches }: MediaQueryListEvent) =>
setMatches(matches);

if (mediaQueryList != undefined) {
setMatches(mediaQueryList.matches);
try {
mediaQueryList.addEventListener('change', handleChange);
} catch {
// Safari 13 fallback
mediaQueryList.addListener?.(handleChange);
}
}

return () => {
try {
mediaQueryList?.removeEventListener('change', handleChange);
} catch {
// Safari 13 fallback
mediaQueryList?.removeListener?.(handleChange);
}
};
const getSnapshot = React.useCallback(() => {
return typeof window !== 'undefined'
? window.matchMedia?.(queryString).matches
: undefined;
}, [queryString]);

return !!matches;
const subscribe = React.useCallback(
(onChange: () => void) => {
const mediaQueryList = window.matchMedia?.(queryString);
mediaQueryList?.addEventListener?.('change', onChange);
r100-stack marked this conversation as resolved.
Show resolved Hide resolved
return () => mediaQueryList?.removeEventListener?.('change', onChange);
},
[queryString],
);

return useSyncExternalStore(subscribe, getSnapshot, () => undefined);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
const _React = React; // prevent bundlers from stripping the namespace import

/**
* Wrapper around `React.useSyncExternalStore` that uses a shim for React 17.
*/
export const useSyncExternalStore =
_React.useSyncExternalStore || useSyncExternalStoreShim;

// ----------------------------------------------------------------------------

/**
* The shim below is adapted from React's source to make it ESM-compatible.
*
* Note: This does not use `getServerSnapshot` at all, because there is
* apparently no way to check "hydrating" state in pre-18.
*
* @see https://github.com/facebook/react/tree/main/packages/use-sync-external-store
*/
function useSyncExternalStoreShim<T>(
subscribe: (onSubscribe: () => void) => () => void,
getSnapshot: () => T,
): T {
const value = getSnapshot();
const [{ instance }, forceUpdate] = React.useState({
instance: { value, getSnapshot },
});
r100-stack marked this conversation as resolved.
Show resolved Hide resolved

React.useLayoutEffect(() => {
r100-stack marked this conversation as resolved.
Show resolved Hide resolved
instance.value = value;
instance.getSnapshot = getSnapshot;

if (!Object.is(value, getSnapshot())) {
forceUpdate({ instance });
}
}, [subscribe, value, getSnapshot]); // eslint-disable-line

React.useEffect(() => {
const synchronize = () => {
if (!Object.is(instance.value, instance.getSnapshot())) {
forceUpdate({ instance });
}
};
synchronize();
return subscribe(synchronize);
}, [subscribe]); // eslint-disable-line

return value;
}
Loading