You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
useControl<MapboxDraw>(...) fails on mount in a React 19 + Next.js 16 + Turbopack stack with the error:
TypeError: Cannot read properties of undefined (reading 'get')
at draw.add(feature)
The error originates in @mapbox/mapbox-gl-draw's api.add (line 87 of src/api.js), which calls ctx.store.get(feature.id). ctx.store is undefined (not null) at the moment of the call, meaning the draw instance's onAdd lifecycle hook never executed before draw.add was invoked.
A manual useEffect that calls map.getMap().addControl(draw) and then draw.add(feature) directly (bypassing useControl entirely) does not exhibit the bug under the same stack. This narrows the bug to useControl's lifecycle handling specifically, not to mapbox-gl-draw or the broader pattern of mounting mapbox-gl primitives from React effects.
Environment
react-map-gl@^7.1.9 (issue likely also affects @vis.gl/react-mapbox@8.x per related issues below)
@mapbox/mapbox-gl-draw@^1.5.1
mapbox-gl@^3.22.0
react@19.2.3
next@16.1.7 (App Router, Turbopack dev mode, default StrictMode)
We hit the error during a feature-flagged editor migration to mapbox-gl-draw. Five fix iterations across four mitigation strategies, all with the error appearing at the same call site (draw.add(feature) in the parent's hydration useEffect):
Sync setDrawReady(true) from props.onCreate inside useControl's factory. Errored: Cannot update a component while rendering a different component (factory runs in useMemo during render, and the parent's setState from inside the child's render-phase factory is illegal).
queueMicrotask(() => setDrawReady(true)). First error cleared. New error surfaced: Cannot read properties of undefined (reading 'get') from draw.add. Also fixed a second error (You must provide a featureId to enter direct_select mode) by switching the constructor's defaultMode from a custom direct_select-extending mode to simple_select.
Added a drawSourcesReady gate driven by a one-shot 'idle' event listener registered inside onLoad, in addition to mapLoaded and drawReady. Hypothesis: the map's idle event fires only after both react-map-gl's onLoad and mapbox-gl-draw's internal load-handler complete, so by then _ctx.store should be initialised. Outcome: error persisted at the same site.
Ported to the documented useControl(onCreate, onAdd, onRemove) third-overload recipe. Factory returns the MapboxDraw instance only; event wiring and parent-notification (props.onCreate(draw)) moved into onAdd so the parent receives the draw instance only after useControl's useEffect has invoked map.addControl(draw) and triggered MapboxDraw.onAdd(map) to set _ctx.store. Outcome: error persisted at the same site, despite the lifecycle ordering being structurally correct on paper.
After the fifth iteration we abandoned the migration and ran an empirical PoC: a manual useEffect-based mount that bypasses useControl. The PoC succeeds 10/10 across hard-refresh and client-side-navigation stress cycles under the same stack.
Workaround (manual addControl in useEffect)
This pattern works in the same stack where useControl<MapboxDraw> fails. Posted as data, not as a recommendation for the upstream API:
The empirical asymmetry (useEffect-driven addControl works; useControl-driven addControl does not) under the same React/Next/Turbopack stack suggests the bug lives in useControl's useMemo + useEffect choreography rather than in mapbox-gl-draw or in the broader pattern.
Two non-exclusive hypotheses:
useMemo factory orphans under React 19 StrictMode. React 19 invokes useMemo's calculator twice in StrictMode dev (the second result is returned). If the first factory call constructs a MapboxDraw that the parent ever holds a reference to (via a side effect in the factory body, or via some other capture path), and that reference's onAdd is never called by useControl's useEffect (which uses the second result), then draw.add(...) on the first instance throws because _ctx.store is undefined.
Manual useEffect mount avoids both: the draw instance is created in setup, used in setup, and torn down in cleanup, all in a single closure. There is no orphaned reference held by the parent and no separate factory-vs-effect timing.
The four issues plus this one suggest a class of failures, not isolated bugs. The common factor across all five: react-map-gl's effect-mounted child primitives misbehave under React 19's StrictMode + Next.js 16's Turbopack and Activity machinery.
Background
Filing as community contribution from a Front Carbon (CCS planning B2B SaaS) Editor 2.0 migration that abandoned useControl<MapboxDraw> after five iterations and pivoted to manual useEffect-based mount. The minimal reproducible code above is from our internal sandbox PoC (10/10 green for the manual pattern; 0/5 green for the useControl pattern under the same stack). Happy to provide a full minimal-reproduction repository if it helps narrow down the root cause.
Not blocking on a fix from our side; the workaround is acceptable for our use case. Posting in case it helps the maintainers correlate this with the four related open issues into a single root-cause investigation.
Description
useControl<MapboxDraw>(...)fails on mount in a React 19 + Next.js 16 + Turbopack stack with the error:The error originates in
@mapbox/mapbox-gl-draw'sapi.add(line 87 ofsrc/api.js), which callsctx.store.get(feature.id).ctx.storeisundefined(notnull) at the moment of the call, meaning the draw instance'sonAddlifecycle hook never executed beforedraw.addwas invoked.A manual
useEffectthat callsmap.getMap().addControl(draw)and thendraw.add(feature)directly (bypassinguseControlentirely) does not exhibit the bug under the same stack. This narrows the bug touseControl's lifecycle handling specifically, not to mapbox-gl-draw or the broader pattern of mounting mapbox-gl primitives from React effects.Environment
react-map-gl@^7.1.9(issue likely also affects@vis.gl/react-mapbox@8.xper related issues below)@mapbox/mapbox-gl-draw@^1.5.1mapbox-gl@^3.22.0react@19.2.3next@16.1.7(App Router, Turbopack dev mode, default StrictMode)Diagnostic strategies attempted (all failed)
We hit the error during a feature-flagged editor migration to mapbox-gl-draw. Five fix iterations across four mitigation strategies, all with the error appearing at the same call site (
draw.add(feature)in the parent's hydrationuseEffect):setDrawReady(true)fromprops.onCreateinsideuseControl's factory. Errored:Cannot update a component while rendering a different component(factory runs inuseMemoduring render, and the parent'ssetStatefrom inside the child's render-phase factory is illegal).queueMicrotask(() => setDrawReady(true)). First error cleared. New error surfaced:Cannot read properties of undefined (reading 'get')fromdraw.add. Also fixed a second error (You must provide a featureId to enter direct_select mode) by switching the constructor'sdefaultModefrom a custom direct_select-extending mode tosimple_select.drawSourcesReadygate driven by a one-shot'idle'event listener registered insideonLoad, in addition tomapLoadedanddrawReady. Hypothesis: the map'sidleevent fires only after both react-map-gl'sonLoadand mapbox-gl-draw's internal load-handler complete, so by then_ctx.storeshould be initialised. Outcome: error persisted at the same site.useControl(onCreate, onAdd, onRemove)third-overload recipe. Factory returns theMapboxDrawinstance only; event wiring and parent-notification (props.onCreate(draw)) moved intoonAddso the parent receives the draw instance only afteruseControl'suseEffecthas invokedmap.addControl(draw)and triggeredMapboxDraw.onAdd(map)to set_ctx.store. Outcome: error persisted at the same site, despite the lifecycle ordering being structurally correct on paper.After the fifth iteration we abandoned the migration and ran an empirical PoC: a manual
useEffect-based mount that bypassesuseControl. The PoC succeeds 10/10 across hard-refresh and client-side-navigation stress cycles under the same stack.Workaround (manual
addControlinuseEffect)This pattern works in the same stack where
useControl<MapboxDraw>fails. Posted as data, not as a recommendation for the upstream API:Reproducible failing example
The same component but using
useControl<MapboxDraw>(onCreate, onAdd, onRemove)(the third-overload recipe) fails. Minimal failing pattern:Hypothesised root cause
The empirical asymmetry (
useEffect-drivenaddControlworks;useControl-drivenaddControldoes not) under the same React/Next/Turbopack stack suggests the bug lives inuseControl'suseMemo+useEffectchoreography rather than in mapbox-gl-draw or in the broader pattern.Two non-exclusive hypotheses:
useMemofactory orphans under React 19 StrictMode. React 19 invokesuseMemo's calculator twice in StrictMode dev (the second result is returned). If the first factory call constructs aMapboxDrawthat the parent ever holds a reference to (via a side effect in the factory body, or via some other capture path), and that reference'sonAddis never called byuseControl'suseEffect(which uses the second result), thendraw.add(...)on the first instance throws because_ctx.storeisundefined.Effect-ordering interleaving with Turbopack hot-mount cycles. Issues [Bug] Marker crashes with "appendChild" error during rapid client-side navigation (React 19 / Next.js 16) #2584 and [Bug] Marker crashes with "appendChild" on Activity reappear when Next.js
cacheComponentsis enabled (production-only) #2588 document the same family of failures acrossMarker(addTo),Source/Layer(addSource/addLayer), where the new mount's effects fire before the previous mount's cleanup runs.useControl's lifecycle may have a similar interleave window.Manual
useEffectmount avoids both: the draw instance is created in setup, used in setup, and torn down in cleanup, all in a single closure. There is no orphaned reference held by the parent and no separate factory-vs-effect timing.Related open issues
useControl + MapboxDrawbroke on Next 14.1 → 14.2 upgrade. Open since 2024-07-08, no maintainer response.MarkercrashesCannot read properties of undefined (reading 'appendChild')on rapid client-side navigation in React 19 + Next 16. Open since 2026-04-03; confirmed by a second reporter.cacheComponentsis enabled (production-only) #2588 —Markercrashes oncacheComponentsActivity reappear in React 19 + Next 16.sourcesupplied toReact.Fragment. React.Fragment can only havekeyandchildrenprops." #2410 — React 19 +Source/LayerFragment-prop warnings.The four issues plus this one suggest a class of failures, not isolated bugs. The common factor across all five: react-map-gl's effect-mounted child primitives misbehave under React 19's StrictMode + Next.js 16's Turbopack and Activity machinery.
Background
Filing as community contribution from a Front Carbon (CCS planning B2B SaaS) Editor 2.0 migration that abandoned
useControl<MapboxDraw>after five iterations and pivoted to manualuseEffect-based mount. The minimal reproducible code above is from our internal sandbox PoC (10/10 green for the manual pattern; 0/5 green for theuseControlpattern under the same stack). Happy to provide a full minimal-reproduction repository if it helps narrow down the root cause.Not blocking on a fix from our side; the workaround is acceptable for our use case. Posting in case it helps the maintainers correlate this with the four related open issues into a single root-cause investigation.