glideapps / glideapps/glide-data-grid

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

Open Beginner friendly
#1,198 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
5.3k
Forks
428
PR merge metrics
No merged PRs in 30d

Description

### Summary

Under React StrictMode (i.e. any development build wrapped in ``), the
accessibility tree never renders — `document.querySelectorAll('[role="gridcell"]').length`
stays `0` for the lifetime of the grid. Screen readers get an unlabelled ``, 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):

```js
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:

```js
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 `` inside `` and, once cells are painted, evaluate
`document.querySelectorAll('[role="gridcell"]').length` → `0`. Remove `` and the
same page returns one node per visible cell, with the expected accessible text.

### Suggested fix

Re-arm the ref on mount:

```js
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.

Contributor guide

Open the contributing guide

Research direction

Start by inspecting useDebouncedMemo in dist/esm/common/utils.js and its accessibilityTree consumer in internal/data-grid/data-grid.js. Reproduce the issue with a DataEditor inside React StrictMode, then verify that visible cells produce [role="gridcell"] nodes and accessible text after the fix.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
accessibility, frontend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.