Skip to content

Conversation

@GeoffCoxMSFT
Copy link
Member

Problem

The large number of no-op implementation for the default value of the custom hooks is preventing bundling minification.

Changes

  • used a proxy to no-op every custom style hook for the default value

Issues

Fixes #27139

@GeoffCoxMSFT GeoffCoxMSFT self-assigned this Mar 31, 2023
@GeoffCoxMSFT GeoffCoxMSFT requested a review from a team as a code owner March 31, 2023 18:01
@github-actions github-actions bot added this to the March Project Cycle Q1 2023 milestone Mar 31, 2023
@codesandbox-ci
Copy link

codesandbox-ci bot commented Mar 31, 2023

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 07e0d30:

Sandbox Source
@fluentui/react 8 starter Configuration
@fluentui/react-components 9 starter Configuration

@size-auditor
Copy link

size-auditor bot commented Mar 31, 2023

Asset size changes

Size Auditor did not detect a change in bundle size for any component!

Baseline commit: 34d618882a79b1cd65b2d4903f77caf9d5d83891 (build)

@fabricteam
Copy link
Collaborator

fabricteam commented Mar 31, 2023

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-accordion
Accordion (including children components)
88.882 kB
26.551 kB
86.215 kB
26.105 kB
-2.667 kB
-446 B
react-alert
Alert
96.647 kB
23.133 kB
93.98 kB
22.654 kB
-2.667 kB
-479 B
react-avatar
Avatar
60.312 kB
15.579 kB
57.645 kB
15.103 kB
-2.667 kB
-476 B
react-avatar
AvatarGroup
18.178 kB
6.683 kB
15.593 kB
6.246 kB
-2.585 kB
-437 B
react-avatar
AvatarGroupItem
76.573 kB
20.086 kB
73.906 kB
19.61 kB
-2.667 kB
-476 B
react-badge
Badge
25.85 kB
7.585 kB
23.265 kB
7.161 kB
-2.585 kB
-424 B
react-badge
CounterBadge
26.833 kB
7.889 kB
24.166 kB
7.462 kB
-2.667 kB
-427 B
react-badge
PresenceBadge
34.458 kB
8.693 kB
31.791 kB
8.264 kB
-2.667 kB
-429 B
react-button
Button
39.749 kB
9.959 kB
37.082 kB
9.502 kB
-2.667 kB
-457 B
react-button
CompoundButton
46.921 kB
11.428 kB
44.254 kB
10.996 kB
-2.667 kB
-432 B
react-button
MenuButton
44.437 kB
11.306 kB
41.77 kB
10.83 kB
-2.667 kB
-476 B
react-button
SplitButton
52.931 kB
12.872 kB
50.264 kB
12.408 kB
-2.667 kB
-464 B
react-button
ToggleButton
58.121 kB
11.878 kB
55.454 kB
11.445 kB
-2.667 kB
-433 B
react-checkbox
Checkbox
36.067 kB
10.856 kB
33.4 kB
10.417 kB
-2.667 kB
-439 B
react-checkbox
CheckboxField
43.019 kB
12.787 kB
40.352 kB
12.332 kB
-2.667 kB
-455 B
react-combobox
Combobox (including child components)
88.73 kB
28.105 kB
86.063 kB
27.578 kB
-2.667 kB
-527 B
react-combobox
ComboboxField
85.147 kB
27.871 kB
82.48 kB
27.415 kB
-2.667 kB
-456 B
react-combobox
Dropdown (including child components)
87.428 kB
27.853 kB
84.761 kB
27.34 kB
-2.667 kB
-513 B
react-components
react-components: Button, FluentProvider & webLightTheme
67.994 kB
18.395 kB
65.327 kB
17.903 kB
-2.667 kB
-492 B
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
207.4 kB
57.516 kB
204.733 kB
57.09 kB
-2.667 kB
-426 B
react-components
react-components: FluentProvider & webLightTheme
38.819 kB
12.351 kB
36.234 kB
11.918 kB
-2.585 kB
-433 B
react-datepicker-compat
DatePicker Compat
250.208 kB
66.95 kB
247.541 kB
66.478 kB
-2.667 kB
-472 B
react-dialog
Dialog (including children components)
93.388 kB
27.408 kB
90.721 kB
26.984 kB
-2.667 kB
-424 B
react-divider
Divider
19.863 kB
6.697 kB
17.278 kB
6.262 kB
-2.585 kB
-435 B
react-field
Field
20.552 kB
7.092 kB
17.885 kB
6.653 kB
-2.667 kB
-439 B
react-image
Image
14.011 kB
4.993 kB
11.426 kB
4.558 kB
-2.585 kB
-435 B
react-infobutton
InfoButton
130.577 kB
39.439 kB
127.91 kB
38.984 kB
-2.667 kB
-455 B
react-infobutton
InfoLabel
133.874 kB
40.488 kB
131.289 kB
40.034 kB
-2.585 kB
-454 B
react-input
Input
25.747 kB
7.697 kB
23.08 kB
7.276 kB
-2.667 kB
-421 B
react-input
InputField
35.723 kB
10.637 kB
33.056 kB
10.224 kB
-2.667 kB
-413 B
react-label
Label
12.57 kB
4.586 kB
9.985 kB
4.148 kB
-2.585 kB
-438 B
react-menu
Menu (including children components)
130.926 kB
39.629 kB
128.259 kB
39.172 kB
-2.667 kB
-457 B
react-menu
Menu (including selectable components)
134.062 kB
40.161 kB
131.395 kB
39.689 kB
-2.667 kB
-472 B
react-persona
Persona
67.338 kB
17.524 kB
64.671 kB
17.052 kB
-2.667 kB
-472 B
react-popover
Popover
117.58 kB
35.828 kB
114.913 kB
35.404 kB
-2.667 kB
-424 B
react-progress
ProgressBar
15.961 kB
5.675 kB
13.376 kB
5.248 kB
-2.585 kB
-427 B
react-progress
ProgressField
26.435 kB
8.859 kB
23.768 kB
8.417 kB
-2.667 kB
-442 B
react-provider
FluentProvider
20.766 kB
7.119 kB
18.181 kB
6.687 kB
-2.585 kB
-432 B
react-radio
Radio
35.48 kB
11.126 kB
32.895 kB
10.681 kB
-2.585 kB
-445 B
react-radio
RadioGroup
17.988 kB
6.544 kB
15.321 kB
6.109 kB
-2.667 kB
-435 B
react-radio
RadioGroupField
28.166 kB
9.735 kB
25.499 kB
9.284 kB
-2.667 kB
-451 B
react-select
Select
26.957 kB
8.766 kB
24.29 kB
8.336 kB
-2.667 kB
-430 B
react-select
SelectField
36.293 kB
11.316 kB
33.626 kB
10.882 kB
-2.667 kB
-434 B
react-slider
Slider
35.919 kB
11.059 kB
33.252 kB
10.627 kB
-2.667 kB
-432 B
react-slider
SliderField
45.847 kB
14.005 kB
43.18 kB
13.559 kB
-2.667 kB
-446 B
react-spinbutton
SpinButton
35.648 kB
10.348 kB
32.981 kB
9.923 kB
-2.667 kB
-425 B
react-spinbutton
SpinButtonField
44.671 kB
12.812 kB
42.004 kB
12.368 kB
-2.667 kB
-444 B
react-spinner
Spinner
23.425 kB
7.195 kB
20.84 kB
6.752 kB
-2.585 kB
-443 B
react-switch
Switch
31.413 kB
9.294 kB
28.828 kB
8.847 kB
-2.585 kB
-447 B
react-switch
SwitchField
38.317 kB
11.21 kB
35.65 kB
10.783 kB
-2.667 kB
-427 B
react-table
DataGrid
149.878 kB
40.662 kB
147.211 kB
40.179 kB
-2.667 kB
-483 B
react-table
Table (Primitives only)
47.119 kB
12.703 kB
44.534 kB
12.278 kB
-2.585 kB
-425 B
react-table
Table as DataGrid
138.018 kB
35.282 kB
135.351 kB
34.812 kB
-2.667 kB
-470 B
react-table
Table (Selection only)
85.849 kB
21.303 kB
83.264 kB
20.865 kB
-2.585 kB
-438 B
react-table
Table (Sort only)
85.179 kB
21.113 kB
82.594 kB
20.669 kB
-2.585 kB
-444 B
react-text
Text - Default
15.018 kB
5.327 kB
12.433 kB
4.896 kB
-2.585 kB
-431 B
react-textarea
Textarea
29.07 kB
9.081 kB
26.485 kB
8.656 kB
-2.585 kB
-425 B
react-textarea
TextareaField
39.454 kB
12.055 kB
36.787 kB
11.613 kB
-2.667 kB
-442 B
react-tooltip
Tooltip
49.388 kB
16.815 kB
46.721 kB
16.36 kB
-2.667 kB
-455 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-card
Card - All
83.489 kB
23.894 kB
react-card
Card
78.308 kB
22.431 kB
react-card
CardFooter
9.035 kB
3.799 kB
react-card
CardHeader
10.959 kB
4.503 kB
react-card
CardPreview
9.84 kB
4.153 kB
react-link
Link
12.301 kB
5.07 kB
react-portal
Portal
11.649 kB
4.263 kB
react-portal-compat
PortalCompatProvider
6.446 kB
2.185 kB
react-positioning
usePositioning
24.008 kB
8.798 kB
react-text
Text - Wrappers
15.572 kB
5.23 kB
🤖 This report was generated against 34d618882a79b1cd65b2d4903f77caf9d5d83891

@fabricteam
Copy link
Collaborator

fabricteam commented Mar 31, 2023

Perf Analysis (@fluentui/react-components)

No significant results to display.

All results

Scenario Render type Master Ticks PR Ticks Iterations Status
Avatar mount 881 907 5000
Button mount 590 583 5000
Field mount 1540 1544 5000
FluentProvider mount 1091 1079 5000
FluentProviderWithTheme mount 292 297 10
FluentProviderWithTheme virtual-rerender 278 284 10
FluentProviderWithTheme virtual-rerender-with-unmount 290 288 10
InfoButton mount 200 203 5000
MakeStyles mount 1407 1409 50000
Persona mount 2126 2103 5000
SpinButton mount 1833 1851 5000

/**
* @internal
*/
export const CustomStyleHooksContext = React.createContext<CustomStyleHooksContextValue | undefined>(undefined);
Copy link
Collaborator

@fabricteam fabricteam Mar 31, 2023

Choose a reason for hiding this comment

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

🕵 fluentuiv9 Open the Visual Regressions report to inspect the 1 screenshots

✅ There was 0 screenshots added, 0 screenshots removed, 1925 screenshots unchanged, 0 screenshots with different dimensions and 1 screenshots with visible difference.

unknown 1 screenshots
Image Name Diff(in Pixels) Image Type
FluentProvider CustomStyleHooks.SplitButton.chromium.png 1605723 Changed

const preProxyDefaultValue: Partial<CustomStyleHooksContextValue> = {};

const defaultValueProxyHandler: ProxyHandler<CustomStyleHooksContextValue> = {
get(_target, _prop, _receiver) {
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
get(_target, _prop, _receiver) {
get() {

@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Added proxy for default custom hook noop",
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
"comment": "Added proxy for default custom hook noop",
"comment": "fix: Use proxy for default custom hook noop",

ling1726
ling1726 previously approved these changes Apr 3, 2023
@ling1726 ling1726 self-requested a review April 3, 2023 08:37
@ling1726 ling1726 dismissed their stale review April 3, 2023 08:37

Needs more investigation

},
};

const customStyleHooksContextDefaultValue = new Proxy(
Copy link
Member

@layershifter layershifter Apr 3, 2023

Choose a reason for hiding this comment

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

@GeoffCoxMSFT I don't think that it works as expected as after a shallow merge in FluentProvider we are losing the proxy functionality. An example is below:

const noop = () => { };
const preProxyDefaultValue = {};

const defaultValueProxyHandler = {
    get() {
        return noop;
    },
};

const customStyleHooksContextDefaultValue = new Proxy(
  preProxyDefaultValue,
  defaultValueProxyHandler
);

// --- 

function shallowMerge(a, b) {
  // Merge impacts perf: we should like to avoid it if it's possible
  if (a && b) {
    return { ...a, ...b };
  }

  if (a) {
    return a;
  }

  return b;
}

// ---

const hooksFromProps = {
  useButton: 'hook',
  useImage: 'hook',
}
const result = shallowMerge(customStyleHooksContextDefaultValue, hooksFromProps) 

⬇️⬇️⬇️

result.useButton // string 'hook' ✅

customStyleHooksContextDefaultValue.useFoo // function ✅ 
result.useFoo // undefined 🟥  We lost the proxy

Copy link
Member Author

Choose a reason for hiding this comment

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

@layershifter Have you done much work with proxies in the past? I'm not sure what the fix would be here if spread breaks the proxy. Should we do object.assign instead?

Copy link
Member

Choose a reason for hiding this comment

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

@GeoffCoxMSFT FYI: I just merged your PR with tests #27086, so we have a good base to test against 🎉


Option 1

There are few to be aware:

  • There is no isProxy() in JS, so we don't know with what we are dealing
  • Object spread will not work in in this case

To workaround that we can expose merge() function on the context:

// packages/react-components/react-shared-contexts/src/CustomStyleHooksContext/CustomStyleHooksContext.ts
  useDataGridHeaderCellStyles_unstable: CustomStyleHook;
  useDataGridSelectionCellStyles_unstable: CustomStyleHook;

+ merge: (overrides: CustomStyleHooksContextValue | Partial<CustomStyleHooksContextValue> | undefined) => void;
};

And then modify the handler:

const defaultValueProxyHandler: ProxyHandler<CustomStyleHooksContextValue> = {
  get(_target, _prop, _receiver) {
    if (_prop === 'merge') {
      return (overrides: CustomStyleHooksContextValue | Partial<CustomStyleHooksContextValue> | undefined) => {
        if (overrides) {
          Object.assign(preProxyDefaultValue, overrides);
        }
      };
    }

    if (_target[_prop]) {
      return _target[_prop];
    }

    return noop;
  },
};

(in this case we mutate target object + we are considering target as a source of hooks)

Then in useFluentProvider:

// packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts
-  // parentCustomStyleHooks will not be a partial
-  const mergedCustomStyleHooks = shallowMerge(
-    parentCustomStyleHooks,
-    customStyleHooks_unstable,
-  ) as CustomStyleHooksContextValue;
+. parentCustomStyleHooks.merge(customStyleHooks_unstable);

// ...
-  customStyleHooks_unstable: mergedCustomStyleHooks,
+  customStyleHooks_unstable: parentCustomStyleHooks,

Option 2

I talked with @ling1726 and his a better idea that does not require Proxy and IMO is clearer.

// The list of hooks is built from the exports from react-components/src/index
type ComponentNames = 'Button' | 'Image' // etc.

type CustomStyleHooksContextValue = {
  (componentName: ComponentNames): CustomStyleHook;
  current: Partial<Record<ComponentNames, CustomStyleHook>>;
};

/**
 * @internal
 */
export const CustomStyleHooksContext = React.createContext<CustomStyleHooksContextValue | undefined>(undefined);

const noop = () => {};

const customStyleHooksContextDefaultValue: CustomStyleHooksContextValue = componentName => {
  return customStyleHooksContextDefaultValue.current[componentName] || noop;
};

customStyleHooksContextDefaultValue.current = {};

In this case merging will be just:

const parentCustomStyleHooks = useCustomStyleHooks();

parentCustomStyleHooks.current = {
  ...parentCustomStyleHooks,
  ...props.customStyleHooks,
}

But it will require changes in components, for example:

-const { useButtonStyles_unstable: useCustomStyles } = useCustomStyleHooks_unstable();
-useCustomStyles(state);
+const useCustomStyles = useCustomStyleHooks_unstable();
+useCustomStyles('Button')(state)

I would say that this approach clearer, but it's up to you to decide 🐱

@GeoffCoxMSFT
Copy link
Member Author

Closing in favor of #27491

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.

Custom style hooks adds object with properties that can't be minified by Terser

5 participants