Imported from delacournz/delacour-ui (
packages/native-ui/src/components/provider/AGENTS.md). Install upstream withnpx skills add delacournz/delacour-ui --skill provider. Copyright stays with the author.
DelacourProvider
Every provider an app needs at its root, in one component. Mount it once, around
everything — a root layout, an App.tsx.
import { DelacourProvider } from "@delacour/react-native-ui/provider";
Files
| File | What it holds |
|---|---|
index.ts |
→ @delacour/react-native-ui/provider |
provider.tsx |
DelacourProvider — the app's root layer stack; its displayName is DelacourUI.Provider, because prefix-plus-symbol would stutter and this matches its export subpath |
Design
- Five layers, outermost first:
GestureHandlerRootView→SafeAreaProvider→KeyboardProvider→<KeyboardStateSync />beside the children →BottomSheetModalProvideraround them. The order is not stylistic. The gesture root has to be an ancestor native view of every handler aPressablecreates, and its absence is silent — no error, no warning, presses simply stop landing.KeyboardStateSynchas to be a CHILD ofKeyboardProvider, because it callsuseKeyboardContext(), and it stays a SIBLING of the modal provider rather than moving inside it: the repair is global and has to outlive any layer that can remount. initialMetricsdefaults toinitialWindowMetrics, and that is load-bearing.SafeAreaProviderrendersnull— not unstyled children, nothing — until its native view reports the firstonInsetsChange, so without the seed every cold start shows a blank frame. The seed is a snapshot taken at native module init, so it is stale when the app launches into a rotated or split-screen window — stale for exactly one commit, because the native measurement overwrites it. A blank frame on every launch is the worse trade.initialMetrics={null}opts out: a default parameter only fires onundefined, sonullis a value rather than an absence, the same rulepressedScalefollows.stylereaches the gesture root and carries no default.GestureHandlerRootViewapplies its own{ flex: 1 }wheneverstyleis undefined, so merging one in here would both duplicate it and make the prop behave differently than it does upstream. Pass a style and thatflex: 1is gone — include it yourself.- No per-layer escape hatches, and no layer-named props. No
gestureHandler={false}, nosafeAreaProps, nokeyboardProps. A boolean that turns off the gesture root has "nothing responds to a press" as its failure mode, which is the least debuggable outcome in the package. And a prop surface that names the layers changes shape every time a layer is added:children,styleandinitialMetricssay nothing about what is inside, so a futureBottomSheetModalProvideror portal host is an edit to one file rather than a breaking change. An app that genuinely needs a different stack composes the providers by hand — they are all public from their own packages, and this is a convenience, not a gate. - A new layer goes innermost. Anything that draws above the app — a
bottom-sheet modal provider, a portal host, a toast host — has to sit inside
every provider it reads, so it wraps
{children}and nothing else moves. A layer that brings a new native peer is a peer-dependency decision first.BottomSheetModalProvideris the case this rule was written for and now the case it governs: it reads the gesture root for its pan, the safe area for its insets and the keyboard values a sheet's footer rides, so it is last. @gorhom/bottom-sheetis a required peer because of this component, on exactly the argumentreact-native-keyboard-controlleralready carries. It was optional while nothing imported it. The recommended root now does, so every app resolves it, and a flag saying otherwise would only suppress the install warning that explains the Metro resolution error coming out of the app's root layout. It is pure JavaScript over Reanimated and Gesture Handler — both required peers already — so this costs an install and no native build. Rule 3's promise survives intact:/buttonstill pulls nothing sheet-related.- Deliberately not idempotent. It does not detect an enclosing copy of
itself. Nesting
GestureHandlerRootViewcosts aView; nestingSafeAreaProviderseeds from the parent's insets and costs a native view; nestingKeyboardProvideris the one that actually breaks — two pairs of shared values, two sets of native observers, and the outerKeyboardStateSyncrepairing values nobody reads. That is also the only layer that cannot be detected:useKeyboardContext()returns a module-private default object outside a provider, that object is not exported, and the hookconsole.warns whenever it hands one back, so a detection read would print a warning in every correctly-mounted app. A guard covering the two harmless layers and missing the harmful one is worse than none — it teaches callers that nesting is fine. react-native-keyboard-controlleris a required peer because of this component. It was optional whileScreenwas the only importer: an app that never importedScreennever resolved it. The recommended root now imports it, so every app resolves it, and the flag had stopped describing reality — all it still did was suppress the install warning that would have explained the Metro resolution error coming out of the app's root layout. Rule 3's promise is per-subpath and survives intact:/buttonstill pulls nothing keyboard-related.- Nothing here for
bun test, and noprovider.variants.tsto give it some. The component is four nested elements and one default parameter; extracting aresolveInitialMetrics()would be a unit test of??. The rule that pure decisions live in a*.variants.tshas no decision here to relocate. - No gallery route. See Adding a component.