From 67b419d0c5a54ccab6b3833116fab04646b92471 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Wed, 3 Apr 2024 16:09:09 +0200 Subject: [PATCH 1/2] chore: rework context selector --- ...-c9b7337d-ff30-4b45-b59c-9792fbfbe26e.json | 7 ++ ...-d923ecc1-4ad2-40d8-b0ab-7cc73fec6bc4.json | 7 ++ ...-b5e8c2e6-b9c0-459a-b70d-f6e5b996fe26.json | 7 ++ ...-63eccfb5-066c-4b6e-b2a9-14eba8432844.json | 7 ++ ...-cbe80143-ea17-482d-bdd9-e6de909501af.json | 7 ++ ...-02145d63-5afa-42af-b10c-7111592ff2da.json | 7 ++ ...-688ad41a-ce88-44f9-b22b-827659bf73f8.json | 7 ++ ...-924543e0-fb7a-4df7-a41d-464baeb12b9e.json | 7 ++ ...-8d29f6f2-2b84-4961-b9d6-03626bf0fbb9.json | 7 ++ ...-93f5be04-0f57-4217-970a-6be348bac3d2.json | 7 ++ ...-4a8b2c1f-03c6-4612-9af2-bd280d65359d.json | 7 ++ ...-1e5d7c8c-5e88-4dd2-a137-3474dc420422.json | 7 ++ .../src/global-context-selector.ts | 2 +- .../library/etc/react-accordion.api.md | 6 +- .../library/etc/react-avatar.api.md | 6 +- .../library/etc/react-combobox.api.md | 8 +- .../etc/react-context-selector.api.md | 19 +--- .../src/createContext.test.tsx | 1 + .../src/createContext.ts | 80 +++++++------ .../react-context-selector/src/index.ts | 2 +- .../react-context-selector/src/types.ts | 30 +---- .../src/useContextSelector.ts | 107 +++++++----------- .../src/useHasParentContext.ts | 8 +- .../library/etc/react-dialog.api.md | 2 +- .../react-menu/library/etc/react-menu.api.md | 4 +- .../library/etc/react-popover.api.md | 4 +- .../library/etc/react-swatch-picker.api.md | 2 +- .../library/etc/react-table.api.md | 6 +- .../react-tabs/library/etc/react-tabs.api.md | 6 +- .../react-tree/library/etc/react-tree.api.md | 2 +- 30 files changed, 210 insertions(+), 169 deletions(-) create mode 100644 change/@fluentui-global-context-c9b7337d-ff30-4b45-b59c-9792fbfbe26e.json create mode 100644 change/@fluentui-react-accordion-d923ecc1-4ad2-40d8-b0ab-7cc73fec6bc4.json create mode 100644 change/@fluentui-react-avatar-b5e8c2e6-b9c0-459a-b70d-f6e5b996fe26.json create mode 100644 change/@fluentui-react-combobox-63eccfb5-066c-4b6e-b2a9-14eba8432844.json create mode 100644 change/@fluentui-react-context-selector-cbe80143-ea17-482d-bdd9-e6de909501af.json create mode 100644 change/@fluentui-react-dialog-02145d63-5afa-42af-b10c-7111592ff2da.json create mode 100644 change/@fluentui-react-menu-688ad41a-ce88-44f9-b22b-827659bf73f8.json create mode 100644 change/@fluentui-react-popover-924543e0-fb7a-4df7-a41d-464baeb12b9e.json create mode 100644 change/@fluentui-react-swatch-picker-8d29f6f2-2b84-4961-b9d6-03626bf0fbb9.json create mode 100644 change/@fluentui-react-table-93f5be04-0f57-4217-970a-6be348bac3d2.json create mode 100644 change/@fluentui-react-tabs-4a8b2c1f-03c6-4612-9af2-bd280d65359d.json create mode 100644 change/@fluentui-react-tree-1e5d7c8c-5e88-4dd2-a137-3474dc420422.json diff --git a/change/@fluentui-global-context-c9b7337d-ff30-4b45-b59c-9792fbfbe26e.json b/change/@fluentui-global-context-c9b7337d-ff30-4b45-b59c-9792fbfbe26e.json new file mode 100644 index 00000000000000..32d9d4b664a84a --- /dev/null +++ b/change/@fluentui-global-context-c9b7337d-ff30-4b45-b59c-9792fbfbe26e.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "chore: update types", + "packageName": "@fluentui/global-context", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-accordion-d923ecc1-4ad2-40d8-b0ab-7cc73fec6bc4.json b/change/@fluentui-react-accordion-d923ecc1-4ad2-40d8-b0ab-7cc73fec6bc4.json new file mode 100644 index 00000000000000..f07736e795bc7e --- /dev/null +++ b/change/@fluentui-react-accordion-d923ecc1-4ad2-40d8-b0ab-7cc73fec6bc4.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: update types", + "packageName": "@fluentui/react-accordion", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-avatar-b5e8c2e6-b9c0-459a-b70d-f6e5b996fe26.json b/change/@fluentui-react-avatar-b5e8c2e6-b9c0-459a-b70d-f6e5b996fe26.json new file mode 100644 index 00000000000000..d40a92c7e2685f --- /dev/null +++ b/change/@fluentui-react-avatar-b5e8c2e6-b9c0-459a-b70d-f6e5b996fe26.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: update types", + "packageName": "@fluentui/react-avatar", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-combobox-63eccfb5-066c-4b6e-b2a9-14eba8432844.json b/change/@fluentui-react-combobox-63eccfb5-066c-4b6e-b2a9-14eba8432844.json new file mode 100644 index 00000000000000..b7c9cb04fb9c35 --- /dev/null +++ b/change/@fluentui-react-combobox-63eccfb5-066c-4b6e-b2a9-14eba8432844.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: update types", + "packageName": "@fluentui/react-combobox", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-context-selector-cbe80143-ea17-482d-bdd9-e6de909501af.json b/change/@fluentui-react-context-selector-cbe80143-ea17-482d-bdd9-e6de909501af.json new file mode 100644 index 00000000000000..c3ee6b68a7fe40 --- /dev/null +++ b/change/@fluentui-react-context-selector-cbe80143-ea17-482d-bdd9-e6de909501af.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: rework useContextSelector()", + "packageName": "@fluentui/react-context-selector", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-dialog-02145d63-5afa-42af-b10c-7111592ff2da.json b/change/@fluentui-react-dialog-02145d63-5afa-42af-b10c-7111592ff2da.json new file mode 100644 index 00000000000000..7d6a3d9526b072 --- /dev/null +++ b/change/@fluentui-react-dialog-02145d63-5afa-42af-b10c-7111592ff2da.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: update types", + "packageName": "@fluentui/react-dialog", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-menu-688ad41a-ce88-44f9-b22b-827659bf73f8.json b/change/@fluentui-react-menu-688ad41a-ce88-44f9-b22b-827659bf73f8.json new file mode 100644 index 00000000000000..1fa7ba8f7e0adc --- /dev/null +++ b/change/@fluentui-react-menu-688ad41a-ce88-44f9-b22b-827659bf73f8.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: update types", + "packageName": "@fluentui/react-menu", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-popover-924543e0-fb7a-4df7-a41d-464baeb12b9e.json b/change/@fluentui-react-popover-924543e0-fb7a-4df7-a41d-464baeb12b9e.json new file mode 100644 index 00000000000000..adda47aa3fb0bc --- /dev/null +++ b/change/@fluentui-react-popover-924543e0-fb7a-4df7-a41d-464baeb12b9e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: update types", + "packageName": "@fluentui/react-popover", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-swatch-picker-8d29f6f2-2b84-4961-b9d6-03626bf0fbb9.json b/change/@fluentui-react-swatch-picker-8d29f6f2-2b84-4961-b9d6-03626bf0fbb9.json new file mode 100644 index 00000000000000..b8ae5fc2a91d95 --- /dev/null +++ b/change/@fluentui-react-swatch-picker-8d29f6f2-2b84-4961-b9d6-03626bf0fbb9.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: update types", + "packageName": "@fluentui/react-swatch-picker", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-table-93f5be04-0f57-4217-970a-6be348bac3d2.json b/change/@fluentui-react-table-93f5be04-0f57-4217-970a-6be348bac3d2.json new file mode 100644 index 00000000000000..4819d520daa9e3 --- /dev/null +++ b/change/@fluentui-react-table-93f5be04-0f57-4217-970a-6be348bac3d2.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: update types", + "packageName": "@fluentui/react-table", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-tabs-4a8b2c1f-03c6-4612-9af2-bd280d65359d.json b/change/@fluentui-react-tabs-4a8b2c1f-03c6-4612-9af2-bd280d65359d.json new file mode 100644 index 00000000000000..47524c2cc3745a --- /dev/null +++ b/change/@fluentui-react-tabs-4a8b2c1f-03c6-4612-9af2-bd280d65359d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: update types", + "packageName": "@fluentui/react-tabs", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-tree-1e5d7c8c-5e88-4dd2-a137-3474dc420422.json b/change/@fluentui-react-tree-1e5d7c8c-5e88-4dd2-a137-3474dc420422.json new file mode 100644 index 00000000000000..a1a6954b0379c4 --- /dev/null +++ b/change/@fluentui-react-tree-1e5d7c8c-5e88-4dd2-a137-3474dc420422.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: update types", + "packageName": "@fluentui/react-tree", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/global-context/src/global-context-selector.ts b/packages/react-components/global-context/src/global-context-selector.ts index 591a1544925d70..c5c661f4dbc2fc 100644 --- a/packages/react-components/global-context/src/global-context-selector.ts +++ b/packages/react-components/global-context/src/global-context-selector.ts @@ -45,7 +45,7 @@ export const createContext = (defaultValue: T, name: string, packageName: str const globalSymbols = Object.getOwnPropertySymbols(globalObject); if (!globalSymbols.includes(sym)) { // eslint-disable-next-line @fluentui/no-context-default-value - globalObject[sym] = baseCreateContext(defaultValue); + globalObject[sym] = baseCreateContext(defaultValue) as React.Context; } return globalObject[sym] as React.Context; diff --git a/packages/react-components/react-accordion/library/etc/react-accordion.api.md b/packages/react-components/react-accordion/library/etc/react-accordion.api.md index af9164de7a81a9..91d4f5b9ef8e6c 100644 --- a/packages/react-components/react-accordion/library/etc/react-accordion.api.md +++ b/packages/react-components/react-accordion/library/etc/react-accordion.api.md @@ -10,12 +10,12 @@ import type { ARIAButtonSlotProps } from '@fluentui/react-aria'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import { ContextSelector } from '@fluentui/react-context-selector'; -import { FC } from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { JSXElementConstructor } from 'react'; import type { PresenceMotionSlotProps } from '@fluentui/react-motion'; -import { Provider } from 'react'; import { ProviderProps } from 'react'; import * as React_2 from 'react'; +import { ReactElement } from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; @@ -158,7 +158,7 @@ export type AccordionProps = ComponentProps> & FC>>; +export const AccordionProvider: (props: ProviderProps>) => ReactElement>; // @public (undocumented) export type AccordionSlots = { diff --git a/packages/react-components/react-avatar/library/etc/react-avatar.api.md b/packages/react-components/react-avatar/library/etc/react-avatar.api.md index 44f5da7d971dc9..c5ccf587652d0c 100644 --- a/packages/react-components/react-avatar/library/etc/react-avatar.api.md +++ b/packages/react-components/react-avatar/library/etc/react-avatar.api.md @@ -9,14 +9,14 @@ import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import { ContextSelector } from '@fluentui/react-context-selector'; -import { FC } from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { JSXElementConstructor } from 'react'; import type { PopoverProps } from '@fluentui/react-popover'; import type { PopoverSurface } from '@fluentui/react-popover'; import { PresenceBadge } from '@fluentui/react-badge'; -import { Provider } from 'react'; import { ProviderProps } from 'react'; import * as React_2 from 'react'; +import { ReactElement } from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; import type { TooltipProps } from '@fluentui/react-tooltip'; @@ -113,7 +113,7 @@ export type AvatarGroupProps = ComponentProps & { }; // @public (undocumented) -export const AvatarGroupProvider: Provider & FC>; +export const AvatarGroupProvider: (props: ProviderProps) => ReactElement>; // @public (undocumented) export type AvatarGroupSlots = { diff --git a/packages/react-components/react-combobox/library/etc/react-combobox.api.md b/packages/react-components/react-combobox/library/etc/react-combobox.api.md index 0af54ff732ec9d..e70a255b439b9b 100644 --- a/packages/react-components/react-combobox/library/etc/react-combobox.api.md +++ b/packages/react-components/react-combobox/library/etc/react-combobox.api.md @@ -15,13 +15,13 @@ import { ContextSelector } from '@fluentui/react-context-selector'; import { EventData } from '@fluentui/react-utilities'; import { EventHandler } from '@fluentui/react-utilities'; import type { ExtractSlotProps } from '@fluentui/react-utilities'; -import { FC } from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { JSXElementConstructor } from 'react'; import { PortalProps } from '@fluentui/react-portal'; import type { PositioningShorthand } from '@fluentui/react-positioning'; -import { Provider } from 'react'; import { ProviderProps } from 'react'; import * as React_2 from 'react'; +import { ReactElement } from 'react'; import type { Slot } from '@fluentui/react-utilities'; import { SlotClassNames } from '@fluentui/react-utilities'; import type { SlotComponentType } from '@fluentui/react-utilities'; @@ -87,7 +87,7 @@ export type ComboboxProps = Omit, 'input'> }; // @public @deprecated (undocumented) -export const ComboboxProvider: Provider & FC>; +export const ComboboxProvider: (props: ProviderProps) => ReactElement>; // @public (undocumented) export type ComboboxSlots = { @@ -162,7 +162,7 @@ export type ListboxProps = ComponentProps & SelectionProps & { }; // @public (undocumented) -export const ListboxProvider: React_2.Provider & React_2.FC>; +export const ListboxProvider: (props: React_2.ProviderProps) => React_2.ReactElement>; // @public (undocumented) export type ListboxSlots = { diff --git a/packages/react-components/react-context-selector/etc/react-context-selector.api.md b/packages/react-components/react-context-selector/etc/react-context-selector.api.md index 781ff6f659b93b..54a580c906a137 100644 --- a/packages/react-components/react-context-selector/etc/react-context-selector.api.md +++ b/packages/react-components/react-context-selector/etc/react-context-selector.api.md @@ -7,9 +7,8 @@ import * as React_2 from 'react'; // @internal (undocumented) -export type Context = React_2.Context & { - Provider: React_2.FC>; - Consumer: never; +export type Context = Omit>, 'Consumer' | 'Provider'> & { + Provider: (props: React_2.ProviderProps) => React_2.ReactElement; }; // @public (undocumented) @@ -17,19 +16,11 @@ export type ContextSelector = (value: Value) => SelectedVa // @internal (undocumented) export type ContextValue = { - listeners: ((payload: readonly [ContextVersion, Value]) => void)[]; - value: React_2.MutableRefObject; - version: React_2.MutableRefObject; + value: Value; + subscribe: (listener: () => void) => () => void; + notify?: () => void; }; -// @internal (undocumented) -export type ContextValues = ContextValue & { - listeners: ((payload: readonly [ContextVersion, Record]) => void)[]; -}; - -// @internal (undocumented) -export type ContextVersion = number; - // @internal (undocumented) export const createContext: (defaultValue: Value) => Context; diff --git a/packages/react-components/react-context-selector/src/createContext.test.tsx b/packages/react-components/react-context-selector/src/createContext.test.tsx index da630ff14a7dad..4bc474373c3506 100644 --- a/packages/react-components/react-context-selector/src/createContext.test.tsx +++ b/packages/react-components/react-context-selector/src/createContext.test.tsx @@ -4,6 +4,7 @@ import * as ReactIs from 'react-is'; describe('createContext', () => { it('creates a Provider component', () => { const Context = createContext(null); + expect(ReactIs.isValidElementType(Context.Provider)).toBeTruthy(); }); }); diff --git a/packages/react-components/react-context-selector/src/createContext.ts b/packages/react-components/react-context-selector/src/createContext.ts index 4a0cbddf915cb2..6053c7da2e93ca 100644 --- a/packages/react-components/react-context-selector/src/createContext.ts +++ b/packages/react-components/react-context-selector/src/createContext.ts @@ -1,39 +1,49 @@ import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; import * as React from 'react'; -import { unstable_NormalPriority as NormalPriority, unstable_runWithPriority as runWithPriority } from 'scheduler'; -import { Context, ContextValue } from './types'; +import type { Context, ContextValue } from './types'; const createProvider = (Original: React.Provider>) => { const Provider: React.FC> = props => { - // Holds an actual "props.value" - const valueRef = React.useRef(props.value); - // Used to sync context updates and avoid stale values, can be considered as render/effect counter of Provider. - const versionRef = React.useRef(0); + 'use no memo'; - // A stable object, is used to avoid context updates via mutation of its values. - const contextValue = React.useRef>(); + const [store] = React.useState(() => { + const listeners = new Set(); - if (!contextValue.current) { - contextValue.current = { - value: valueRef, - version: versionRef, - listeners: [], + return { + value: props.value, + + subscribe: (listener: Function) => { + listeners.add(listener); + + return () => { + listeners.delete(listener); + }; + }, + + notify: () => { + for (const listener of listeners) { + listener(); + } + }, }; - } + }); - useIsomorphicLayoutEffect(() => { - valueRef.current = props.value; - versionRef.current += 1; + store.value = props.value; - runWithPriority(NormalPriority, () => { - (contextValue.current as ContextValue).listeners.forEach(listener => { - listener([versionRef.current, props.value]); - }); - }); - }, [props.value]); + useIsomorphicLayoutEffect( + () => { + // if (!Object.is(store.value, props.value)) { + store.value = props.value; + store.notify(); + // } + }, + // "store" is a constant object, so it's safe to omit it from the dependencies + // eslint-disable-next-line react-hooks/exhaustive-deps + [props.value], + ); - return React.createElement(Original, { value: contextValue.current }, props.children); + return React.createElement(Original, { value: store }, props.children); }; /* istanbul ignore else */ @@ -41,7 +51,7 @@ const createProvider = (Original: React.Provider>) => Provider.displayName = 'ContextSelector.Provider'; } - return Provider as unknown as React.Provider>; + return Provider; }; /** @@ -49,16 +59,16 @@ const createProvider = (Original: React.Provider>) => */ export const createContext = (defaultValue: Value): Context => { // eslint-disable-next-line @fluentui/no-context-default-value - const context = React.createContext>({ - value: { current: defaultValue }, - version: { current: -1 }, - listeners: [], + const originalContext = React.createContext>({ + value: defaultValue, + subscribe: () => () => { + /* noop */ + }, }); - context.Provider = createProvider(context.Provider); - - // We don't support Consumer API - delete (context as unknown as Context).Consumer; - - return context as unknown as Context; + return Object.assign(originalContext, { + Provider: createProvider(originalContext.Provider), + // We don't support Consumer API + Consumer: undefined, + }); }; diff --git a/packages/react-components/react-context-selector/src/index.ts b/packages/react-components/react-context-selector/src/index.ts index bbc42ef78afce3..700c10680a4bff 100644 --- a/packages/react-components/react-context-selector/src/index.ts +++ b/packages/react-components/react-context-selector/src/index.ts @@ -2,4 +2,4 @@ export { createContext } from './createContext'; export { useContextSelector } from './useContextSelector'; export { useHasParentContext } from './useHasParentContext'; // eslint-disable-next-line @fluentui/ban-context-export -export type { Context, ContextSelector, ContextValue, ContextValues, ContextVersion } from './types'; +export type { Context, ContextSelector, ContextValue } from './types'; diff --git a/packages/react-components/react-context-selector/src/types.ts b/packages/react-components/react-context-selector/src/types.ts index 29ffae061ec368..5b679b7b5a7fb1 100644 --- a/packages/react-components/react-context-selector/src/types.ts +++ b/packages/react-components/react-context-selector/src/types.ts @@ -3,36 +3,18 @@ import * as React from 'react'; /** * @internal */ -export type Context = React.Context & { - Provider: React.FC>; - Consumer: never; +export type Context = Omit>, 'Consumer' | 'Provider'> & { + // eslint-disable-next-line @typescript-eslint/naming-convention + Provider: (props: React.ProviderProps) => React.ReactElement; }; export type ContextSelector = (value: Value) => SelectedValue; -/** - * @internal - */ -export type ContextVersion = number; - /** * @internal */ export type ContextValue = { - /** Holds a set of subscribers from components. */ - listeners: ((payload: readonly [ContextVersion, Value]) => void)[]; - - /** Holds an actual value of React's context that will be propagated down for computations. */ - value: React.MutableRefObject; - - /** A version field is used to sync a context value and consumers. */ - version: React.MutableRefObject; -}; - -/** - * @internal - */ -export type ContextValues = ContextValue & { - /** List of listners to publish changes */ - listeners: ((payload: readonly [ContextVersion, Record]) => void)[]; + value: Value; + subscribe: (listener: () => void) => () => void; + notify?: () => void; }; diff --git a/packages/react-components/react-context-selector/src/useContextSelector.ts b/packages/react-components/react-context-selector/src/useContextSelector.ts index 6d72026936e326..dc6d194e78611a 100644 --- a/packages/react-components/react-context-selector/src/useContextSelector.ts +++ b/packages/react-components/react-context-selector/src/useContextSelector.ts @@ -1,7 +1,18 @@ -import { useEventCallback, useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; +import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; import * as React from 'react'; -import { Context, ContextSelector, ContextValue, ContextVersion } from './types'; +import type { Context, ContextSelector, ContextValue } from './types'; + +function checkIfSnapshotChanged(inst: { value: T; getSnapshot: () => T }): boolean { + const latestGetSnapshot = inst.getSnapshot; + const prevValue = inst.value; + try { + const nextValue = latestGetSnapshot(); + return !Object.is(prevValue, nextValue); + } catch (error) { + return true; + } +} /** * @internal @@ -13,72 +24,40 @@ export const useContextSelector = ( context: Context, selector: ContextSelector, ): SelectedValue => { - const contextValue = React.useContext(context as unknown as Context>); - - const { - value: { current: value }, - version: { current: version }, - listeners, - } = contextValue; - const selected = selector(value); - - const [state, setState] = React.useState([value, selected]); - const dispatch = ( - payload: - | undefined // undefined from render below - | readonly [ContextVersion, Value], // from provider effect - ) => { - setState(prevState => { - if (!payload) { - // early bail out when is dispatched during render - return [value, selected] as const; - } - - if (payload[0] <= version) { - if (Object.is(prevState[1], selected)) { - return prevState; // bail out - } - - return [value, selected] as const; - } - - try { - if (Object.is(prevState[0], payload[1])) { - return prevState; // do not update - } + 'use no memo'; - const nextSelected = selector(payload[1]); + const store = React.useContext(context as React.Context>); + const getSnapshot = () => selector(store.value); - if (Object.is(prevState[1], nextSelected)) { - return prevState; // do not update - } - - return [payload[1], nextSelected] as const; - } catch (e) { - // ignored (stale props or some other reason) - } - - // explicitly spread to enforce typing - return [prevState[0], prevState[1]] as const; // schedule update - }); - }; - - if (!Object.is(state[1], selected)) { - // schedule re-render - // this is safe because it's self contained - dispatch(undefined); - } - - const stableDispatch = useEventCallback(dispatch); + const value = getSnapshot(); + const [{ inst }, forceUpdate] = React.useState({ + inst: { value, getSnapshot }, + }); useIsomorphicLayoutEffect(() => { - listeners.push(stableDispatch); - - return () => { - const index = listeners.indexOf(stableDispatch); - listeners.splice(index, 1); + inst.value = value; + inst.getSnapshot = getSnapshot; + + if (checkIfSnapshotChanged(inst)) { + forceUpdate({ inst }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value, getSnapshot]); + + React.useEffect(() => { + if (checkIfSnapshotChanged(inst)) { + forceUpdate({ inst }); + } + + const handleStoreChange = () => { + if (checkIfSnapshotChanged(inst)) { + forceUpdate({ inst }); + } }; - }, [stableDispatch, listeners]); - return state[1] as SelectedValue; + return store.subscribe(handleStoreChange); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [store]); + + return value; }; diff --git a/packages/react-components/react-context-selector/src/useHasParentContext.ts b/packages/react-components/react-context-selector/src/useHasParentContext.ts index 2ebdf51a868c12..19034236c57632 100644 --- a/packages/react-components/react-context-selector/src/useHasParentContext.ts +++ b/packages/react-components/react-context-selector/src/useHasParentContext.ts @@ -10,11 +10,7 @@ import { Context, ContextValue } from './types'; * @returns whether the hook is wrapped by a parent context */ export function useHasParentContext(context: Context) { - const contextValue = React.useContext(context as unknown as Context>); + const contextValue = React.useContext(context as React.Context>); - if (contextValue.version) { - return contextValue.version.current !== -1; - } - - return false; + return Boolean(contextValue.notify); } diff --git a/packages/react-components/react-dialog/library/etc/react-dialog.api.md b/packages/react-components/react-dialog/library/etc/react-dialog.api.md index 5c331b7f8f0b14..989e757f83fad7 100644 --- a/packages/react-components/react-dialog/library/etc/react-dialog.api.md +++ b/packages/react-components/react-dialog/library/etc/react-dialog.api.md @@ -135,7 +135,7 @@ export type DialogProps = ComponentProps> & { }; // @public (undocumented) -export const DialogProvider: React_2.Provider & React_2.FC>; +export const DialogProvider: (props: React_2.ProviderProps) => React_2.ReactElement>; // @public (undocumented) export type DialogSlots = { diff --git a/packages/react-components/react-menu/library/etc/react-menu.api.md b/packages/react-components/react-menu/library/etc/react-menu.api.md index 10ce069c886e84..75652fe175e904 100644 --- a/packages/react-components/react-menu/library/etc/react-menu.api.md +++ b/packages/react-components/react-menu/library/etc/react-menu.api.md @@ -246,7 +246,7 @@ export type MenuListProps = ComponentProps & { }; // @public (undocumented) -export const MenuListProvider: React_2.Provider & React_2.FC>; +export const MenuListProvider: (props: React_2.ProviderProps) => React_2.ReactElement>; // @public (undocumented) export type MenuListSlots = { @@ -345,7 +345,7 @@ export type MenuProps = ComponentProps & Pick & React_2.FC>; +export const MenuProvider: (props: React_2.ProviderProps) => React_2.ReactElement>; // @public (undocumented) export type MenuSlots = {}; diff --git a/packages/react-components/react-popover/library/etc/react-popover.api.md b/packages/react-components/react-popover/library/etc/react-popover.api.md index e6cca50af5bddc..ebe123eab070cb 100644 --- a/packages/react-components/react-popover/library/etc/react-popover.api.md +++ b/packages/react-components/react-popover/library/etc/react-popover.api.md @@ -9,13 +9,11 @@ import { ARIAButtonType } from '@fluentui/react-aria'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { ContextSelector } from '@fluentui/react-context-selector'; -import { FC } from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import { JSXElementConstructor } from 'react'; import type { PortalProps } from '@fluentui/react-portal'; import type { PositioningShorthand } from '@fluentui/react-positioning'; import type { PositioningVirtualElement } from '@fluentui/react-positioning'; -import { Provider } from 'react'; import { ProviderProps } from 'react'; import * as React_2 from 'react'; import { ReactElement } from 'react'; @@ -64,7 +62,7 @@ export type PopoverProps = Pick & { }; // @public (undocumented) -export const PopoverProvider: Provider & FC>; +export const PopoverProvider: (props: ProviderProps) => ReactElement>; // @public export type PopoverSize = 'small' | 'medium' | 'large'; diff --git a/packages/react-components/react-swatch-picker/library/etc/react-swatch-picker.api.md b/packages/react-components/react-swatch-picker/library/etc/react-swatch-picker.api.md index 67bb133c76f0b0..b69a9bb9b0be6c 100644 --- a/packages/react-components/react-swatch-picker/library/etc/react-swatch-picker.api.md +++ b/packages/react-components/react-swatch-picker/library/etc/react-swatch-picker.api.md @@ -160,7 +160,7 @@ export type SwatchPickerProps = ComponentProps & { }; // @public (undocumented) -export const SwatchPickerProvider: React_2.Provider & React_2.FC>; +export const SwatchPickerProvider: (props: React_2.ProviderProps) => React_2.ReactElement>; // @public export const SwatchPickerRow: ForwardRefComponent; diff --git a/packages/react-components/react-table/library/etc/react-table.api.md b/packages/react-components/react-table/library/etc/react-table.api.md index c2810b6abdb86c..2044f798d0025a 100644 --- a/packages/react-components/react-table/library/etc/react-table.api.md +++ b/packages/react-components/react-table/library/etc/react-table.api.md @@ -13,12 +13,12 @@ import type { CheckboxProps } from '@fluentui/react-checkbox'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { ContextSelector } from '@fluentui/react-context-selector'; -import { FC } from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import { Provider } from 'react'; +import { JSXElementConstructor } from 'react'; import { ProviderProps } from 'react'; import type { Radio } from '@fluentui/react-radio'; import * as React_2 from 'react'; +import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { SelectionHookParams } from '@fluentui/react-utilities'; import { SelectionMode as SelectionMode_2 } from '@fluentui/react-utilities'; @@ -93,7 +93,7 @@ export type DataGridCellState = TableCellState; export const dataGridClassNames: SlotClassNames; // @public (undocumented) -export const DataGridContextProvider: Provider & FC>; +export const DataGridContextProvider: (props: ProviderProps) => ReactElement>; // @public (undocumented) export type DataGridContextValue = TableFeaturesState & { diff --git a/packages/react-components/react-tabs/library/etc/react-tabs.api.md b/packages/react-components/react-tabs/library/etc/react-tabs.api.md index f7662bb2bfe77e..d7937a460abf02 100644 --- a/packages/react-components/react-tabs/library/etc/react-tabs.api.md +++ b/packages/react-components/react-tabs/library/etc/react-tabs.api.md @@ -7,11 +7,11 @@ import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { ContextSelector } from '@fluentui/react-context-selector'; -import { FC } from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import { Provider } from 'react'; +import { JSXElementConstructor } from 'react'; import { ProviderProps } from 'react'; import * as React_2 from 'react'; +import { ReactElement } from 'react'; import type { Slot } from '@fluentui/react-utilities'; import { SlotClassNames } from '@fluentui/react-utilities'; @@ -78,7 +78,7 @@ export type TabListProps = ComponentProps & { }; // @public (undocumented) -export const TabListProvider: Provider & FC>; +export const TabListProvider: (props: ProviderProps) => ReactElement>; // @public (undocumented) export type TabListSlots = { diff --git a/packages/react-components/react-tree/library/etc/react-tree.api.md b/packages/react-components/react-tree/library/etc/react-tree.api.md index ebb82c1d301fd6..0e22b954d3514a 100644 --- a/packages/react-components/react-tree/library/etc/react-tree.api.md +++ b/packages/react-components/react-tree/library/etc/react-tree.api.md @@ -275,7 +275,7 @@ export type TreeItemProps = ComponentProps> & { }; // @public (undocumented) -export const TreeItemProvider: React_2.Provider & React_2.FC>; +export const TreeItemProvider: (props: React_2.ProviderProps) => React_2.ReactElement>; // @public (undocumented) export type TreeItemSlots = { From 5b28e8aa51a4198d3c7d82c131037c4f6e81e708 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Mon, 5 Aug 2024 18:46:54 +0200 Subject: [PATCH 2/2] fix tests? --- .../react-motion/library/src/hooks/useAnimateAtoms.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.ts b/packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.ts index 66ea0c35e1b6be..dfdde801f6195e 100644 --- a/packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.ts +++ b/packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.ts @@ -89,7 +89,8 @@ function useAnimateAtomsInTestEnvironment() { const realAnimateAtoms = useAnimateAtomsInSupportedEnvironment(); - React.useEffect(() => { + // eslint-disable-next-line no-restricted-properties + React.useLayoutEffect(() => { if (count > 0) { callbackRef.current?.(); }