Writing Custom Stubs
When Peach doesn’t have a builtin stub for a package you use, you can
provide your own through peach.config.ts. Four slots, picked by what
the guest bundle actually requests:
- +
modules: generic Metro module overrides (matched by path fragment) - +
turboModules: TurboModuleRegistry name overrides - +
nativeModules: legacy NativeModules and Expo module name overrides - +
nativeViews: the app’s own native views, replaced by a component
See Configuration for the full surface.
The golden rule: only stub native modules
Never stub a pure JS package. Peach simulates React Native well enough
that pure-JS libraries (even when named react-native-*) work from the
bundle without a stub. Composing View, Text, TouchableOpacity, etc. is
already supported.
Before authoring a stub, verify the package has native code:
- +
NativeModules.X,TurboModuleRegistry.get('X'), orrequireNativeComponent('X')somewhere in its JS - +
ios/orandroid/directory with ObjC/Swift/Java/Kotlin sources - +A
.podspecorbuild.gradlewith native dependencies
Packages that need stubs: react-native-mmkv, react-native-keychain,
@react-native-async-storage/async-storage, expo-haptics,
react-native-fs, etc.
Packages that do not need stubs: @gorhom/bottom-sheet (pure JS on
gesture-handler + reanimated), @gorhom/portal (pure JS context),
react-native-gifted-chat, react-native-calendars, and any pure-JS UI
library. If a pure-JS package doesn’t render correctly under Peach, the
fix is in Peach’s base RN implementation, not in a stub.
modules: generic JS module overrides
Noop (empty module)
For packages you don’t need in the simulator:
Inline (static value)
For config-style packages whose entire surface is data:
Inline also works for deep file paths inside a package. Keys match the Metro module name as a path fragment, so this overrides one file:
File (real implementation, browser-backed)
For packages that need real behavior, point at a project-local file. The dev server compiles it on demand:
File-stub constraints (enforced by the dev middleware, which refuses the compile and says which import it refused):
- +A stub imports
react,react-native,react/jsx-runtime,peach-kit, any package Peach already stubs for this app, and other project files by relative path (./Balance,../shared/format). The simulator hands each package over as a live object, so nothing is read fromnode_modules. - +Relative imports resolve like the app’s own files: the exact path, then
.js,.jsx,.ts,.tsx, then the folder’sindexfile. Each file loads once per launch, so stubs that import the same file share its state. - +No runtime
require()calls - +
.js/.jsx/.ts/.tsxonly
A stub can render any UI react-native can: View, Text, Image,
Pressable, Animated and the rest are the simulator’s own components.
peach-kit adds what a native view uses beyond them:
peach-kit/haptics plays the iOS feedback generators
(haptics.impact('light')), and peach-kit/animation turns an iOS spring
into the physics Animated.spring takes
(Animated.spring(value, { ...spring({ duration: 0.4, bounce: 0.2 }), toValue: 1 })). If
you need a third-party JS lib that Peach does not already stub, vendor the
relevant code into a project file.
For a gradient blur in your own native view, import ProgressiveBlurView from
peach-kit/materials. See Native View Primitives
for its typed props and a nativeViews replacement example.
Use (redirect to an existing compat stub)
If a package’s native surface matches another package Peach already stubs, redirect rather than re-implementing:
false (disable Peach’s builtin stub)
When the upstream package’s JS is correct on its own and you only need Peach to stop redirecting it to a builtin stub:
turboModules and nativeModules: native-seam overrides
If the guest bundle reaches the native side via
TurboModuleRegistry.get('Foo') or NativeModules.Foo, the JS-side
modules slot doesn’t help; you need the native-seam slot.
Mattermost is the canonical example, mixing both:
An app’s own Expo module (a local module under modules/ reached with
requireNativeModule('Name')) goes in nativeModules under the name its
native definition declares with Name("...").
Both slots accept the same ModuleResolution kinds ('noop', false,
{ use }, { file }, { inline }) plus a direct
Record<string, any> native-module object for non-URL configs.
nativeViews: an app’s own native views
peach compat lists an app’s own native modules and views with the
peach.config.ts key each one takes: names passed to
requireNativeModule, TurboModuleRegistry, requireNativeComponent and
requireNativeView, and local Expo modules (a directory with
expo-module.config.json), including those in linked workspace packages.
An app that ships its own native views (a Swift or Kotlin view registered
with requireNativeComponent, or an Expo module view reached with
requireNativeView) gets an empty view in Peach until something renders in
its place. Point the view’s name at a component:
The component receives the props the app passes to the native view. The
file may import react, react-native, peach-kit and the packages
Peach bridges for the app; JSX needs no React import. Render what the
native view shows on device, using the platform primitives peach-kit
exports where one exists (a blur, for example), so the screen keeps its
layout and content.
Authoring guidelines
- +Export the same named exports as the original package (matching its type signatures). If the package’s default export is a function, your stub’s default export should be a function too; the bundle loader preserves callable default exports.
- +Use real browser APIs where they exist: camera →
getUserMedia, clipboard →navigator.clipboard, file picker →<input type="file">, haptics →navigator.vibrate. The goal is real behavior, not a noop. - +For UI components, return a
View/Textwrapper that renderschildrenrather thannull, since most callers depend on layout being preserved. - +For hooks, return sensible defaults that satisfy the type but don’t pretend the native side is doing real work (e.g. permission hooks should return
grantedonly if the browser actually granted the analogous permission). - +Cite the upstream source you matched. Peach’s compat philosophy is “study upstream, mirror exactly”: comments referencing the real package’s file/line make later updates safe.
- +Never stub a pure-JS package. If it doesn’t render, fix Peach’s base RN implementation instead.
Related
- +Configuration guide: full
peach.config.tssurface with demo-app examples - +PeachConfig API: type reference
- +Compat overview: which packages already have builtin stubs