Skip to content

useDebouncedMemo never re-arms mountedRef: accessibility tree never renders under React StrictMode #1198

Description

@shinji-incorta

Summary

Under React StrictMode (i.e. any development build wrapped in <StrictMode>), the
accessibility tree never renders — document.querySelectorAll('[role="gridcell"]').length
stays 0 for the lifetime of the grid. Screen readers get an unlabelled <canvas>, and
tooling that reads the a11y tree (e2e tests, audits) sees nothing. Production builds without
StrictMode are unaffected.

Cause

useDebouncedMemo in dist/esm/common/utils.js (6.0.3):

export function useDebouncedMemo(factory, deps, time) {
    const [state, setState] = React.useState(factory);
    const mountedRef = React.useRef(true);
    React.useEffect(() => () => {
        mountedRef.current = false;          // ← only ever set to false
    }, []);
    const debouncedSetState = React.useRef(debounce(x => {
        if (mountedRef.current) {            // ← gate
            setState(x);
        }
    }, time));
    React.useLayoutEffect(() => {
        if (mountedRef.current) {            // ← gate
            debouncedSetState.current(() => factory());
        }
    }, deps);
    return state;
}

StrictMode mounts, unmounts and remounts each component in development. The cleanup sets
mountedRef.current = false; the remount never sets it back, because the ref is only
initialised on first render. Both guards are then permanently false, so no dependency change
ever produces a new value and the memo keeps whatever factory() returned at first render.

The only consumer is accessibilityTree in internal/data-grid/data-grid.js, whose factory
begins:

if (width < 50 || experimental?.disableAccessibilityTree === true) return null;

At first render the grid is unmeasured (width === 0), so that first value is null — and it
is never replaced.

Evidence

Instrumented 6.0.3 with React 19.2 in Chromium, logging inside the memo and inside
onVisibleRegionChangedImpl:

condition values seen by the memo [role=gridcell]
StrictMode on (as shipped) width: 0, accessibilityHeight: 1 on every run 0
StrictMode on, DataEditor given numeric width/height still width: 0 0
StrictMode on, onVisibleRegionChanged logged clientWidth: 1078, clientHeight: 395 — the measurement does arrive and setClientSize runs 0
StrictMode removed, nothing else changed width: 1022 → 842, accessibilityHeight: 12 36

The third row is the decisive one: width genuinely goes 0 → 1078 via clientSize, and the
memo still never re-runs, even though width is the first entry in its dependency array.

Reproduction

Render any <DataEditor> inside <StrictMode> and, once cells are painted, evaluate
document.querySelectorAll('[role="gridcell"]').length0. Remove <StrictMode> and the
same page returns one node per visible cell, with the expected accessible text.

Suggested fix

Re-arm the ref on mount:

React.useEffect(() => {
    mountedRef.current = true;
    return () => {
        mountedRef.current = false;
    };
}, []);

Alternatively drop the mountedRef guard entirely — since React 18, calling a state setter
after unmount is a no-op rather than a warning, so the guard no longer earns its keep.

Happy to open a PR if that would help. We are carrying the four-line change as a local patch
in the meantime.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions