fix: useContextSelector with React 18
Previous Behavior
useContextSelector() implementation relies on bail-outs if values are shallow equal:
https://github.com/microsoft/fluentui/blob/1f3d2e7acb51e03627e7355e6d73f90bd1faa7e9/packages/react-components/react-context-selector/src/useContextSelector.ts#L47-L49
However, this does not work anymore with React 18 💥
React 17
// Parent: render
// Child: render
// Parent: useLayoutEffect (note 1)
// Child: bail-out ✅
React 18
// Parent: render
// Child: render
// Parent: useLayoutEffect (note 1)
// Child: re-render 💣
Note 1: useLayoutEffect
This effect propagates updates to its consumers once "Parent" gets rendered:
https://github.com/microsoft/fluentui/blob/1f3d2e7acb51e03627e7355e6d73f90bd1faa7e9/packages/react-components/react-context-selector/src/createContext.ts#L25-L34
This results in performance degradation during the initial render 🐌 The change in the behavior was not expected, but it's a part of React 18: https://github.com/facebook/react/pull/22445.
New Behavior
This PR switches useReducer() used in useContextSelector() to useState() that still has the behavior with bailout. In the longterm, we should explore the approach of using useSyncExternalStore() for this functionality.
Perf Analysis (@fluentui/react-components)
No significant results to display.
All results
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| Avatar | mount | 650 | 627 | 5000 | |
| Button | mount | 311 | 321 | 5000 | |
| Field | mount | 1123 | 1158 | 5000 | |
| FluentProvider | mount | 748 | 722 | 5000 | |
| FluentProviderWithTheme | mount | 80 | 82 | 10 | |
| FluentProviderWithTheme | virtual-rerender | 63 | 64 | 10 | |
| FluentProviderWithTheme | virtual-rerender-with-unmount | 73 | 78 | 10 | |
| MakeStyles | mount | 891 | 895 | 50000 | |
| Persona | mount | 1774 | 1800 | 5000 | |
| SpinButton | mount | 1424 | 1413 | 5000 |
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.
📊 Bundle size report
| Package & Exports | Baseline (minified/GZIP) | PR | Change |
|---|---|---|---|
| react-accordion Accordion (including children components) |
99.599 kB30.264 kB |
99.547 kB30.228 kB |
-52 B -36 B |
| react-avatar AvatarGroupItem |
64.829 kB20.272 kB |
64.958 kB20.32 kB |
129 B 48 B |
| react-combobox Combobox (including child components) |
102.735 kB33.197 kB |
102.675 kB33.177 kB |
-60 B -20 B |
| react-combobox Dropdown (including child components) |
104.06 kB33.101 kB |
104 kB33.077 kB |
-60 B -24 B |
| react-components react-components: Accordion, Button, FluentProvider, Image, Menu, Popover |
219.521 kB62.081 kB |
219.456 kB62.061 kB |
-65 B -20 B |
| react-dialog Dialog (including children components) |
100.928 kB29.921 kB |
100.874 kB29.9 kB |
-54 B -21 B |
| react-infobutton InfoButton |
138.694 kB43.395 kB |
138.64 kB43.375 kB |
-54 B -20 B |
| react-infobutton InfoLabel |
142.495 kB44.625 kB |
142.442 kB44.604 kB |
-53 B -21 B |
| react-menu Menu (including children components) |
152.268 kB45.708 kB |
152.208 kB45.678 kB |
-60 B -30 B |
| react-menu Menu (including selectable components) |
154.954 kB46.274 kB |
154.894 kB46.242 kB |
-60 B -32 B |
| react-overflow hooks only |
12.86 kB4.825 kB |
12.813 kB4.81 kB |
-47 B -15 B |
| react-popover Popover |
126.884 kB39.803 kB |
126.83 kB39.78 kB |
-54 B -23 B |
| react-swatch-picker-preview @fluentui/react-swatch-picker-preview - package |
103.385 kB29.763 kB |
103.331 kB29.745 kB |
-54 B -18 B |
| react-table DataGrid |
165.647 kB46.063 kB |
165.596 kB46.024 kB |
-51 B -39 B |
| react-timepicker-compat TimePicker |
104.756 kB34.581 kB |
104.696 kB34.558 kB |
-60 B -23 B |
Unchanged fixtures
| Package & Exports | Size (minified/GZIP) |
|---|---|
| global-context createContext |
510 B328 B |
| global-context createContextSelector |
537 B339 B |
| react-alert Alert |
83.737 kB23.475 kB |
| react-avatar Avatar |
50.175 kB15.944 kB |
| react-avatar AvatarGroup |
19.702 kB7.794 kB |
| react-checkbox Checkbox |
35.656 kB12.07 kB |
| react-components react-components: Button, FluentProvider & webLightTheme |
71.098 kB20.515 kB |
| react-components react-components: FluentProvider & webLightTheme |
43.585 kB14.352 kB |
| react-datepicker-compat DatePicker Compat |
225.764 kB63.187 kB |
| react-field Field |
22.976 kB8.722 kB |
| react-input Input |
28.122 kB9.36 kB |
| react-persona Persona |
57.066 kB17.821 kB |
| react-portal-compat PortalCompatProvider |
7.944 kB2.588 kB |
| react-progress ProgressBar |
17.428 kB6.899 kB |
| react-radio Radio |
32.95 kB10.252 kB |
| react-radio RadioGroup |
15.354 kB6.265 kB |
| react-select Select |
28.609 kB10.204 kB |
| react-slider Slider |
39.949 kB12.968 kB |
| react-spinbutton SpinButton |
36.774 kB11.789 kB |
| react-switch Switch |
35.14 kB11.199 kB |
| react-table Table (Primitives only) |
45.324 kB14.116 kB |
| react-table Table as DataGrid |
136.573 kB36.817 kB |
| react-table Table (Selection only) |
76.334 kB20.553 kB |
| react-table Table (Sort only) |
74.977 kB20.155 kB |
| react-tags InteractionTag |
15.284 kB6.07 kB |
| react-tags Tag |
30.029 kB9.461 kB |
| react-tags TagGroup |
80.68 kB24.073 kB |
| react-textarea Textarea |
30.947 kB10.477 kB |

