Skip to content

feat(react-overflow): allow opting out of first-paint overflow correctness#36281

Draft
bsunderhus wants to merge 6 commits into
microsoft:masterfrom
bsunderhus:react-overflow/pr4-opt-out
Draft

feat(react-overflow): allow opting out of first-paint overflow correctness#36281
bsunderhus wants to merge 6 commits into
microsoft:masterfrom
bsunderhus:react-overflow/pr4-opt-out

Conversation

@bsunderhus
Copy link
Copy Markdown
Contributor

Summary

PR 4 of 4 — DRAFT. Depends on PR 3 (#36264, react-overflow/pr3-first-paint). Do not merge until PR 3 lands — until then the "Files changed" view shows cumulative diffs. Once PR 3 merges this branch will be rebased onto the latest master and the diff will reduce to just the opt-out changes.

Builds on PR 3's first-paint correctness by making that synchronous pass opt-in per consumer, so hot-path clients can skip it without any Overflow prop.

Stack

  1. react-overflow/pr1-strict-mode-lifecycle (refactor(react-overflow,priority-overflow): pure manager + strict-mode-safe lifecycle #36262) — strict-mode-safe lifecycle. Merged.
  2. react-overflow/pr2-subscribe-model (refactor(react-overflow,priority-overflow): subscribe model removes intermediate state from <Overflow> #36263) — subscribe model. Merged.
  3. react-overflow/pr3-first-paint (fix(react-overflow,priority-overflow): correct overflow snapshot on first paint #36264) — first-paint correctness. In review.
  4. This PR — opt-out of first-paint correctness.

How it works

First-paint correctness is now requested by the default item/menu hooks rather than forced unconditionally by the container:

  • useOverflowItem and useOverflowMenu call forceUpdateOverflow() on registration.
  • useOverflowContainer records that request (child effects run before the parent's observe effect) and passes it through as observe(container, { forceUpdate: <requested> }); after observing, forceUpdateOverflow forces directly. The manager still guards the force on the container being measured.
  • A hot-path consumer that builds a custom item hook on top of useOverflowContext and omits the forceUpdateOverflow() call opts the container out of the synchronous first-paint pass entirely — the ResizeObserver then drives the first (async) overflow pass instead.

No new Overflow prop and no new public export — the opt-out is intentionally low-profile.

Test plan

  • Overflow.paint-probe.cy.tsx gains an opt-out case asserting the layout-phase paint is unresolved when items/menu opt out, then resolves asynchronously (5/5 passing).
  • Default first-paint convergence cases unchanged (now item-driven).
  • react-overflow unit / type-check / lint green; react-charts 912 tests / 321 snapshots / 0 axe unaffected.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
priority-overflow
createOverflowManager
5.376 kB
2.176 kB
5.403 kB
2.194 kB
27 B
18 B
react-charts
AreaChart
403.405 kB
125.786 kB
403.478 kB
125.827 kB
73 B
41 B
react-charts
DeclarativeChart
754.408 kB
220.139 kB
754.481 kB
220.175 kB
73 B
36 B
react-charts
DonutChart
313.868 kB
96.529 kB
313.941 kB
96.563 kB
73 B
34 B
react-charts
FunnelChart
305.367 kB
93.359 kB
305.44 kB
93.394 kB
73 B
35 B
react-charts
GanttChart
386.499 kB
120.153 kB
386.572 kB
120.191 kB
73 B
38 B
react-charts
GaugeChart
313.246 kB
95.915 kB
313.319 kB
95.949 kB
73 B
34 B
react-charts
GroupedVerticalBarChart
394.37 kB
122.872 kB
394.443 kB
122.912 kB
73 B
40 B
react-charts
HeatMapChart
388.581 kB
121.173 kB
388.654 kB
121.211 kB
73 B
38 B
react-charts
HorizontalBarChart
293.54 kB
88.973 kB
293.613 kB
89.01 kB
73 B
37 B
react-charts
Legends
233.214 kB
69.846 kB
233.287 kB
69.881 kB
73 B
35 B
react-charts
LineChart
414.729 kB
128.737 kB
414.802 kB
128.77 kB
73 B
33 B
react-charts
PolarChart
342.213 kB
106.408 kB
342.286 kB
106.447 kB
73 B
39 B
react-charts
ScatterChart
394.112 kB
122.856 kB
394.185 kB
122.894 kB
73 B
38 B
react-charts
VerticalBarChart
430.85 kB
127.756 kB
430.923 kB
127.796 kB
73 B
40 B
react-charts
VerticalStackedBarChart
400.574 kB
124.352 kB
400.647 kB
124.385 kB
73 B
33 B
react-components
react-components: entire library
1.295 MB
325.3 kB
1.295 MB
325.345 kB
73 B
45 B
react-overflow
hooks only
9.104 kB
3.234 kB
9.176 kB
3.262 kB
72 B
28 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-breadcrumb
@fluentui/react-breadcrumb - package
102.926 kB
28.76 kB
react-charts
HorizontalBarChartWithAxis
63 B
83 B
react-charts
SankeyChart
211.914 kB
67.836 kB
react-charts
Sparkline
80.503 kB
26.644 kB
react-components
react-components: Button, FluentProvider & webLightTheme
66.328 kB
19.02 kB
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
226.19 kB
67.909 kB
react-components
react-components: FluentProvider & webLightTheme
39.525 kB
13.113 kB
react-headless-components-preview
react-headless-components-preview: entire library
198.183 kB
56.549 kB
react-portal-compat
PortalCompatProvider
5.567 kB
2.237 kB
react-timepicker-compat
TimePicker
104.049 kB
34.748 kB
🤖 This report was generated against 23c05d1de3c0bcb9e84e77d534cbc6df33a974a5

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

Pull request demo site: URL

@@ -0,0 +1,7 @@
{
Copy link
Copy Markdown

@github-actions github-actions Bot Jun 3, 2026

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Charts-DonutChart 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Charts-DonutChart.Dynamic.default.chromium.png 539 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 621 Changed
vr-tests-react-components/Positioning.Positioning end.chromium.png 50 Changed
vr-tests-react-components/ProgressBar converged 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - Dark Mode.default.chromium.png 67 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - High Contrast.default.chromium.png 67 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png 68 Changed
vr-tests-react-components/TagPicker 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - High Contrast.disabled input hover.chromium.png 1319 Changed

There were 3 duplicate changes discarded. Check the build logs for more information.

@bsunderhus bsunderhus force-pushed the react-overflow/pr4-opt-out branch from 3ced633 to 2f9729d Compare June 4, 2026 08:22
bsunderhus and others added 4 commits June 4, 2026 11:55
First-paint correctness is now *requested* by the default item/menu hooks:
useOverflowItem and useOverflowMenu call forceUpdateOverflow on registration,
which the container honors by resolving overflow synchronously in its observe
layout effect (deferring the request until it observes, via child-before-parent
effect ordering). A hot-path consumer that builds a custom item hook on top of
useOverflowContext and omits the forceUpdateOverflow call opts the container out
of the synchronous first-paint pass entirely — the ResizeObserver then drives
the first (async) overflow pass instead. No new Overflow prop or public export;
the opt-out is intentionally low-profile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cleanup now invokes the unregister function returned by registerItem, so
the mocked registerItem must return one too (the real one always does).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Measure only what reaches the screen: a plain requestAnimationFrame loop,
decoupled from React's render/commit/effect cycle, records a filmstrip of every
painted frame. Covers all four opt-out combinations (item/menu x in/out) and
asserts the stable anchors — the first painted frame and the converged final
frame — leaving the timing-dependent intermediate (a transient menu-count
flicker) unasserted so the test does not flake.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bsunderhus bsunderhus force-pushed the react-overflow/pr4-opt-out branch from 6111196 to 9608d91 Compare June 4, 2026 09:58
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bsunderhus bsunderhus force-pushed the react-overflow/pr4-opt-out branch from 9608d91 to 71303ec Compare June 4, 2026 10:56
… hooks

The paint-probe now drives the real OverflowItem and useOverflowMenu. Each
opt-out case is expressed by wrapping the relevant subtree in a provider that
overrides forceUpdateOverflow to a no-op, instead of reimplementing the hooks
to drop that call. A Context.Provider renders no DOM node, so the flex layout
and overflow geometry are unchanged. Same stable filmstrips, now with real
component coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant