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
{{ message }}
Repository navigation
MapView silently ignores overlays wrapped in a Fragment or a custom component #134
<MapView> never renders its children. useCollectedOverlays walks them once with Children.forEach, keeps the elements whose type is one of the five overlay components, and
hands the resulting descriptor arrays to the native view (MapView.tsx:98, :294-297); NativeMapView itself is rendered self-closing (MapView.tsx:272-310). Anything the walk
does not recognise is dropped, and nothing reports that it happened.
Children.forEach has two documented limits
(react.dev — Children, caveats):
it flattens arrays, but it does not traverse Fragments, and it does not render elements,
so it never sees what a component would return. In the collector:
<>…</> / <Fragment key=…> arrives as one element whose type is Symbol(react.fragment). isOverlayChild (useCollectedOverlays.ts:49-62) tests typeof child.type for 'function' / 'object' and then reference equality — a symbol
passes neither, so the Fragment is skipped together with everything inside it.
a wrapper such as <StoreMarkers stores={…} /> arrives as an element whose type is the
wrapper function. It has no static overlayType and is not one of the overlay components,
so it is skipped too. Its render function is never called.
The same goes for any other element (<View>, a <Callout> left over from
react-native-maps): ignored, no __DEV__ warning.
The failure mode is an empty — or partially empty — map with no error, no warning and no
crash, in development and production alike. The shapes that trigger it are ordinary React:
grouping overlays in a component (<StoreMarkers/>, <RouteOverlay/>) — the most common
way to split a map screen up
a render helper that returns <>…</> — renderRoute() returning a <Polyline> plus its
two end <Marker>s
a conditional group: {showRoute && <><Polyline … /><Marker … /></>}
It hits migrations from react-native-maps hardest. There, <MapView> children are rendered
like any other React subtree, so wrapper components and Fragments both work; the same JSX
moved to react-native-better-maps shows a map with nothing on it. Neither the README's
migration table (README.md:362-372), the quick start (README.md:199-231) nor MapViewProps.children (map.ts:46, a plain ReactNode with no JSDoc) says that overlays
have to be direct children.
The example app never exercises this path — every scenario uses the bulk props, and the
only child it ever passes is a single <Geojson> (example/examples/overlaySource.tsx:31-48)
— which is why it went unnoticed.
Reproduction
import{MapView,Marker}from'react-native-better-maps';conststores=[{id: 'a',coordinate: {latitude: 52.2297,longitude: 21.0122}},{id: 'b',coordinate: {latitude: 52.237,longitude: 21.017}},];constregion={latitude: 52.233,longitude: 21.015,latitudeDelta: 0.05,longitudeDelta: 0.05};// 1. Fragment — empty map<MapViewstyle={{flex: 1}}region={region}><>{stores.map((s)=><Markerkey={s.id}id={s.id}coordinate={s.coordinate}/>)}</></MapView>// 2. Wrapper component — empty map; StoreMarkers is never even calledfunctionStoreMarkers(){returnstores.map((s)=><Markerkey={s.id}id={s.id}coordinate={s.coordinate}/>);}<MapViewstyle={{flex: 1}}region={region}><StoreMarkers/></MapView>// 3. Works — the same markers as direct children<MapViewstyle={{flex: 1}}region={region}>{stores.map((s)=><Markerkey={s.id}id={s.id}coordinate={s.coordinate}/>)}
</MapView>
Observed on 2026-09-17 against v1.2.1 (ccbc2a8) in a fresh Expo SDK 57 consumer app:
the Fragment shape on iOS (Apple provider) and the wrapper shape on Android (Google
provider) both render an empty map. The collector is platform-independent, so each shape
fails on both platforms.
Hook-level confirmation, useCollectedOverlays under bun test with React 19.2.3 (hook
invoked with a minimal useRef / useMemo dispatcher — see Regression tests for why a
real test needs an extraction first):
✓ direct array children (items.map) → 2 markers
✓ nested arrays → 3 markers, in order
✗ <>{items.map(…)}</> → 0 markers, console.warn never called
✗ [<Marker/>, <Fragment key="g"><Marker/><Polyline/></>] → 1 marker, 0 polylines, no warning
✗ <MyMarkers /> → 0 markers, MyMarkers never invoked, no warning
✗ [<Marker/>, <NotAnOverlay/>, <div/>] → 1 marker, no warning
Where
package/src/hooks/useCollectedOverlays.ts:209-224 — the Children.forEach walk;
an element no collector matches falls out of the for loop with no else
package/src/hooks/useCollectedOverlays.ts:49-62 — isOverlayChild: static overlayType or reference equality; a symbol type (Fragment) can never match
package/src/components/MapView.tsx:98, :272-310 — the only consumer of children; NativeMapView is rendered without any
package/src/overlays/overlayCollect.ts:32-42 — resolveOverlayId, the marker-N
fallback ids that a Fragment unwrap has to keep stable
package/src/types/map.ts:46 — children?: ReactNode, no JSDoc
README.md:199-231, README.md:362-372, docs/architecture.md:105, package/src/native/README.md:35 — describe the collection, not its limits
Suggested fix
Two halves. Fragments can be supported; wrapper components cannot — the collector runs
inside MapView's render and has no renderer to hand — so they need a warning and a
documented rule.
1. Unwrap Fragments recursively
When child.type === Fragment, recurse into child.props.children instead of testing the
Fragment against the collectors. Depth-first, in document order, so the fallback ids
(marker-0, marker-1, …) stay stable and match what the same markers get as direct
children. Keys play no part in collection — ids come from props, not from key.
Any valid element that is neither a Fragment nor matched by isOverlayChild gets a console.warn through a helper shaped like warnGeojson.ts
(package/src/geojson/warnGeojson.ts). #125 / #128 propose the same warnOverlay helper
for invalid coordinates — share it. Suggested wording:
[react-native-better-maps] MapView: ignoring child <StoreMarkers>. Only <Marker>, <Polyline>,
<Polygon>, <Circle> and <Geojson> are collected, and they must be direct children (Fragments
are unwrapped, other components are not rendered). Return the elements from an array, or use
the bulk `markers` / `polylines` / `polygons` / `circles` props.
Name the offending type (displayName / name for functions, the tag for host elements)
and warn once per type per collection so a list of 500 stray elements does not flood the
console. null, undefined, booleans, strings and numbers stay silent — {cond && <Marker/>}
must not warn.
3. Document the contract
MapViewProps.children JSDoc (map.ts:46): which elements are collected, Fragments are
unwrapped, other components are never rendered.
README quick start: one sentence under the snippet.
README react-native-maps migration table: a row Overlays inside custom components /
render helpers → Not supported — return an array, or use the bulk props.
docs/architecture.md:105 and package/src/native/README.md:35: the same limit next to
the "collected via React.Children" sentence.
Keep children?: ReactNode: a tighter element type cannot express "no wrappers" and would
reject the {cond && <Marker/>} idiom, so the warning is the guard.
TypeScript only — the descriptor structs and the Nitro view spec are untouched, no
nitrogen run.
Regression tests
The walk lives inside useMemo, so today it cannot be called from a bun test without a
React renderer, and the package has neither react-dom nor react-test-renderer. Move it
into a pure collectOverlayChildren(children, state) under package/src/overlays/ — next
to collectGeojsonOverlays.ts, which already has exactly this shape and its own tests — and
keep useCollectedOverlays as the memo wrapper. Then, with createElement alone, in package/src/overlays/__tests__/collectOverlayChildren.test.ts:
a direct array and nested arrays collect everything, in order, with stable marker-N ids
<>…</> and a keyed <Fragment> inside an array collect their contents; a Fragment in a
Fragment works; ids equal the direct-children case
a wrapper component collects nothing, is never invoked, and warns once with its name under __DEV__ = true; nothing is logged with __DEV__ unset
an unknown host element ('div') warns; null / false / strings do not
a mixed list keeps the collected neighbours of an ignored child
Notes
isOverlayChild also accepts any component carrying a static overlayType
(overlayType.ts:17-19), added so reference equality survives Metro resolving the same
component through two paths in a monorepo (overlayType.ts:3-6). It is internal and
undocumented, and it is not a supported way to make wrappers collectable — the wrapper
would have to take exactly MarkerProps, because it is never rendered. The warning must
keep treating those elements as matched.
React's docs mark the Children API as legacy and recommend passing data instead of
elements; the bulk markers / polylines / polygons / circles props are that path
and are the right answer for anyone who hits the wrapper limit.
Problem
<MapView>never renders itschildren.useCollectedOverlayswalks them once withChildren.forEach, keeps the elements whose type is one of the five overlay components, andhands the resulting descriptor arrays to the native view (
MapView.tsx:98,:294-297);NativeMapViewitself is rendered self-closing (MapView.tsx:272-310). Anything the walkdoes not recognise is dropped, and nothing reports that it happened.
Children.forEachhas two documented limits(react.dev —
Children, caveats):it flattens arrays, but it does not traverse Fragments, and it does not render elements,
so it never sees what a component would return. In the collector:
<>…</>/<Fragment key=…>arrives as one element whosetypeisSymbol(react.fragment).isOverlayChild(useCollectedOverlays.ts:49-62) teststypeof child.typefor'function'/'object'and then reference equality — a symbolpasses neither, so the Fragment is skipped together with everything inside it.
<StoreMarkers stores={…} />arrives as an element whosetypeis thewrapper function. It has no static
overlayTypeand is not one of the overlay components,so it is skipped too. Its render function is never called.
The same goes for any other element (
<View>, a<Callout>left over fromreact-native-maps): ignored, no
__DEV__warning.<MapView>children{items.map((i) => <Marker key={i.id} … />)}[[<Marker/>], [<Marker/>, [<Marker/>]]](nested arrays)<>{items.map(…)}</><Marker/>+<Fragment key="group"><Marker/><Polyline/></Fragment><Marker/><StoreMarkers stores={…} /><View/>/ any unknown elementImpact
The failure mode is an empty — or partially empty — map with no error, no warning and no
crash, in development and production alike. The shapes that trigger it are ordinary React:
<StoreMarkers/>,<RouteOverlay/>) — the most commonway to split a map screen up
<>…</>—renderRoute()returning a<Polyline>plus itstwo end
<Marker>s{showRoute && <><Polyline … /><Marker … /></>}It hits migrations from react-native-maps hardest. There,
<MapView>children are renderedlike any other React subtree, so wrapper components and Fragments both work; the same JSX
moved to
react-native-better-mapsshows a map with nothing on it. Neither the README'smigration table (
README.md:362-372), the quick start (README.md:199-231) norMapViewProps.children(map.ts:46, a plainReactNodewith no JSDoc) says that overlayshave to be direct children.
The example app never exercises this path — every scenario uses the bulk props, and the
only child it ever passes is a single
<Geojson>(example/examples/overlaySource.tsx:31-48)— which is why it went unnoticed.
Reproduction
Observed on 2026-09-17 against
v1.2.1(ccbc2a8) in a fresh Expo SDK 57 consumer app:the Fragment shape on iOS (Apple provider) and the wrapper shape on Android (Google
provider) both render an empty map. The collector is platform-independent, so each shape
fails on both platforms.
Hook-level confirmation,
useCollectedOverlaysunderbun testwith React 19.2.3 (hookinvoked with a minimal
useRef/useMemodispatcher — see Regression tests for why areal test needs an extraction first):
Where
package/src/hooks/useCollectedOverlays.ts:209-224— theChildren.forEachwalk;an element no collector matches falls out of the
forloop with noelsepackage/src/hooks/useCollectedOverlays.ts:49-62—isOverlayChild: staticoverlayTypeor reference equality; a symbol type (Fragment) can never matchpackage/src/components/MapView.tsx:98,:272-310— the only consumer ofchildren;NativeMapViewis rendered without anypackage/src/overlays/overlayCollect.ts:32-42—resolveOverlayId, themarker-Nfallback ids that a Fragment unwrap has to keep stable
package/src/types/map.ts:46—children?: ReactNode, no JSDocREADME.md:199-231,README.md:362-372,docs/architecture.md:105,package/src/native/README.md:35— describe the collection, not its limitsSuggested fix
Two halves. Fragments can be supported; wrapper components cannot — the collector runs
inside
MapView's render and has no renderer to hand — so they need a warning and adocumented rule.
1. Unwrap Fragments recursively
When
child.type === Fragment, recurse intochild.props.childreninstead of testing theFragment against the collectors. Depth-first, in document order, so the fallback ids
(
marker-0,marker-1, …) stay stable and match what the same markers get as directchildren. Keys play no part in collection — ids come from props, not from
key.2. Warn in
__DEV__for everything elseAny valid element that is neither a Fragment nor matched by
isOverlayChildgets aconsole.warnthrough a helper shaped likewarnGeojson.ts(
package/src/geojson/warnGeojson.ts). #125 / #128 propose the samewarnOverlayhelperfor invalid coordinates — share it. Suggested wording:
Name the offending type (
displayName/namefor functions, the tag for host elements)and warn once per type per collection so a list of 500 stray elements does not flood the
console.
null,undefined, booleans, strings and numbers stay silent —{cond && <Marker/>}must not warn.
3. Document the contract
MapViewProps.childrenJSDoc (map.ts:46): which elements are collected, Fragments areunwrapped, other components are never rendered.
render helpers → Not supported — return an array, or use the bulk props.
docs/architecture.md:105andpackage/src/native/README.md:35: the same limit next tothe "collected via
React.Children" sentence.Keep
children?: ReactNode: a tighter element type cannot express "no wrappers" and wouldreject the
{cond && <Marker/>}idiom, so the warning is the guard.TypeScript only — the descriptor structs and the Nitro view spec are untouched, no
nitrogen run.
Regression tests
The walk lives inside
useMemo, so today it cannot be called from a bun test without aReact renderer, and the package has neither
react-domnorreact-test-renderer. Move itinto a pure
collectOverlayChildren(children, state)underpackage/src/overlays/— nextto
collectGeojsonOverlays.ts, which already has exactly this shape and its own tests — andkeep
useCollectedOverlaysas the memo wrapper. Then, withcreateElementalone, inpackage/src/overlays/__tests__/collectOverlayChildren.test.ts:marker-Nids<>…</>and a keyed<Fragment>inside an array collect their contents; a Fragment in aFragment works; ids equal the direct-children case
__DEV__ = true; nothing is logged with__DEV__unset'div') warns;null/false/ strings do notNotes
isOverlayChildalso accepts any component carrying a staticoverlayType(
overlayType.ts:17-19), added so reference equality survives Metro resolving the samecomponent through two paths in a monorepo (
overlayType.ts:3-6). It is internal andundocumented, and it is not a supported way to make wrappers collectable — the wrapper
would have to take exactly
MarkerProps, because it is never rendered. The warning mustkeep treating those elements as matched.
ChildrenAPI as legacy and recommend passing data instead ofelements; the bulk
markers/polylines/polygons/circlesprops are that pathand are the right answer for anyone who hits the wrapper limit.
<Marker>; this issue isabout the children of
<MapView>.Acceptance criteria
<MapView><>{markers}</></MapView>renders the same overlays, with the same ids, asthe direct-children form
<Fragment>inside an array of overlays is unwrapped<MapView><StoreMarkers/></MapView>logs one__DEV__warning namingStoreMarkers;nothing is logged in production
{cond && <Marker/>},null, strings and numbers stay silentMapViewProps.children, the README quick start, the migration table and thearchitecture docs state the rule
Related
__DEV__overlay warning helper<Marker>children, out of scope here)