
React Native Best Practices
- 13 installs
- 260 repo stars
- Updated July 29, 2026
- software-mansion-labs/react-native-skills
This is a copy of react-native-best-practices by software-mansion-labs - installs and ranking accrue to the original listing.
Helps with frontend development tasks during AI-assisted development.
About
react-native-best-practices is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted coding.
- react-native-best-practices
- Frontend Development
- AI-coding skill
React Native Best Practices by the numbers
- 13 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/software-mansion-labs/react-native-skills --skill react-native-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 13 |
|---|---|
| repo stars | ★ 260 |
| Last updated | July 29, 2026 |
| Repository | software-mansion-labs/react-native-skills ↗ |
What it does
Helps with frontend development tasks during AI-assisted development.
Files
Animations
Software Mansion's production animation patterns for React Native on Reanimated 4 and the New Architecture.
Load at most one reference file per question. For API signatures and config options, webfetch the documentation pages linked in each reference file.
Critical Rules
- NEVER use `runOnJS`. It is removed in Reanimated 4. Use
scheduleOnRN(fn, ...args)fromreact-native-workletsinstead. This applies everywhere: scroll handlers, gesture callbacks,useAnimatedReaction,useFrameCallback, and any other worklet context.
References
| File | When to read |
|---|---|
animations.md | Choosing between CSS transitions, CSS animations, and shared value animations; CSS transition and CSS animation patterns and rules; animating text; infinite animation cleanup; scheduleOnRN |
animation-functions.md | Gotchas and rules for core hooks (useSharedValue, useAnimatedStyle, useAnimatedProps, useDerivedValue); withSpring config modes; withRepeat and withClamp caveats; composing animations |
layout-animations.md | Entering/exiting animation gotchas (nativeID conflict, view flattening); layout transitions; keyframe animation rules; list item animations (itemLayoutAnimation); shared element transitions |
scroll-and-events.md | Scroll-driven animation patterns (useAnimatedScrollHandler, scrollTo, useScrollOffset); useAnimatedReaction patterns; useFrameCallback; measure rules |
canvas-animations.md | Canvas animations with @shopify/react-native-skia; Reanimated integration (shared values as direct props); interpolateColors; retained vs immediate mode (Picture API); path animations (usePathInterpolation, usePathValue); useClock; gesture integration and element tracking; SKSL runtime shaders and image filters; textures |
canvas-atlas.md | Atlas for batched sprite/tile animation; useTexture, useRSXformBuffer; RSXform matrix format ([scos, ssin, tx, ty]) |
gpu-animations.md | GPU shader animations; react-native-wgpu Canvas and device setup; TypeGPU typed pipelines; Reanimated + WebGPU worklet integration; compute pipelines for particle systems, physics, and simulations; @typegpu/noise (Perlin noise, PRNG); @typegpu/sdf (signed distance shapes); Three.js / React Three Fiber for 3D |
svg-animations.md | Animating SVG elements and paths with Reanimated; createAnimatedComponent for SVG; progress arcs; pulsing circles |
animations-performance.md | Performance tuning; 120fps setup; feature flags; FPS drop fixes; simultaneous animation limits; accessibility (useReducedMotion, ReducedMotionConfig); worklet closure optimization; debug vs release builds |
React Native Best Practices Skill
Production patterns for React Native apps on the New Architecture, by Software Mansion.
Add this skill to give your AI coding agent accurate, current guidance for Software Mansion's React Native libraries: Reanimated, Gesture Handler, React Native SVG, ExecuTorch, Audio API, and more.
Sub-skills
| Sub-skill | Covers | Status |
|---|---|---|
| Animations | Reanimated 4, CSS transitions, CSS animations, shared values, canvas animations (Skia), GPU shader animations (WebGPU, TypeGPU), layout animations, scroll-driven animations, SVG animations, 120fps, performance flags | Complete |
| Gestures | Gesture Handler: tap, pan, pinch, rotation, fling, hover, long press, Pressable, RectButton, Swipeable, Touchable, DrawerLayout, gesture composition, testing | Complete |
| SVG | React Native SVG: when to use vs expo-image/Skia/Lottie/Rive/fonts/WebView, installation, loading (URI/XML/file), touch events, filters, FilterImage, performance pitfalls, iOS memory leaks | Complete |
| On-device AI | React Native ExecuTorch: LLMs (chat, tool calling, structured output, vision-language models), computer vision (classification, object detection, OCR, semantic/instance segmentation, style transfer, embeddings, text-to-image), speech (STT, TTS, VAD), VisionCamera real-time frame processing, model loading, resource management, error handling, custom models | Complete |
| Rich Text | Rich text editing with react-native-enriched, Markdown rendering with react-native-enriched-markdown | Complete |
| Multithreading | react-native-worklets: Worker Runtimes, scheduling APIs, shared memory, Serializable, Synchronizable | Complete |
| Audio | React Native Audio API: playback (buffer sources, oscillators, streaming, queued playback), recording (file, data callback, graph processing), effects (gain, filters, delay, convolver, panner, waveshaper), analysis and visualization, worklets (custom processing, synthesis, UIRuntime/AudioRuntime), system integration (sessions, interruptions, notifications, permissions), testing | Complete |
| JSI | React Native JSI (JavaScript Interface): C++ API for interacting with the JS runtime — jsi::Value, HostObject, HostFunction, NativeState, ArrayBuffer, type casting, threading safety, calling JS from C++, CallInvoker/invokeAsync, performance (zero-copy, PropNameID caching, Scope), setup on Android/iOS, TurboModules vs Nitro Modules vs pure JSI, C++ memory patterns, crash debugging | Complete |
Complete = full reference documentation with code examples.
Structure
react-native-best-practices/
├── SKILL.md # Entry point: routing table for sub-skills
└── references/
├── animations/
│ ├── SKILL.md # When to use, what references to read
│ ├── animations.md # Decision tree, CSS transitions/animations, shared values
│ ├── animation-functions.md # Core hooks, withSpring, withTiming, withDecay, composition
│ ├── layout-animations.md # Entering/exiting, transitions, keyframes
│ ├── scroll-and-events.md # Scroll-driven animations, useAnimatedReaction, useFrameCallback
│ ├── canvas-animations.md # Skia canvas animations, path morphing, SKSL shaders, gesture integration
│ ├── canvas-atlas.md # Atlas batched sprite/tile animation, useTexture, useRSXformBuffer, RSXform
│ ├── gpu-animations.md # Shader animations, react-native-wgpu, TypeGPU, particles
│ ├── svg-animations.md # Animating SVG elements and paths with Reanimated
│ └── animations-performance.md # 120fps, feature flags, simultaneous animation limits
├── gestures/
│ ├── SKILL.md # Version decision tree (v2 Builder vs v3 Hook API)
│ ├── gestures.md # Decision tree, lifecycle, threading, SharedValue config
│ ├── tap-handling.md # RectButton, Pressable, tap, double-tap, hit slop
│ ├── continuous-gestures.md # Pan, Pinch, Rotation, LongPress, Fling, Hover
│ ├── gesture-composition.md # Simultaneous, Race, Exclusive, VirtualGestureDetector
│ ├── swipeable-and-drawer.md # ReanimatedSwipeable, ReanimatedDrawerLayout
│ └── testing.md # Jest setup, fireGestureHandler, troubleshooting
├── svg/
│ ├── SKILL.md # When to use react-native-svg vs alternatives
│ ├── when-to-use.md # Choosing between svg, expo-image, icons, Skia, Lottie
│ └── svg.md # Installation, loading (URI/XML/file), touch events, filters, FilterImage, performance
├── on-device-ai/
│ ├── SKILL.md # Decision tree, critical rules, references routing
│ ├── llm.md # LLM chat (functional/managed), tool calling, structured output, token batching
│ ├── vision.md # Classification, object detection, OCR, segmentation, style transfer, embeddings, VisionCamera
│ ├── speech.md # Speech-to-text (batch/streaming), text-to-speech (batch/streaming), VAD
│ └── setup.md # Installation, resource fetcher, model loading, error handling, custom models
├── rich-text/
│ └── SKILL.md # Editor and renderer patterns, style customization
├── multithreading/
│ ├── SKILL.md # Runtime model, API decision tree, critical rules
│ ├── threading-api.md # Scheduling APIs, Worker Runtimes, sync/async
│ ├── shared-memory.md # Closures, Serializable, Synchronizable
│ └── setup-and-advanced.md # Installation, Babel config, Bundle Mode, Jest
├── audio/
│ ├── SKILL.md # When to use, what references to read
│ ├── audio.md # Decision tree, AudioContext lifecycle, singleton, audio graph, decoding
│ ├── playback.md # AudioBufferSourceNode, OscillatorNode, StreamerNode, queued playback, AudioParam, noise
│ ├── recording.md # AudioRecorder modes (file, callback, graph), permissions, file formats
│ ├── effects-and-analysis.md # GainNode (ADSR), filters, delay, convolver, panner, AnalyserNode, visualization
│ ├── worklets.md # WorkletNode, WorkletSourceNode, WorkletProcessingNode, runtimes, performance
│ └── system-and-notifications.md # AudioManager, sessions, interruptions, notifications, permissions, testing
└── jsi/
├── SKILL.md # Decision tree, references routing
├── overview.md # JSI architecture, sync model, runtime data, prototype manipulation
├── core-types.md # Value, PropNameID, HostFunction, HostObject, NativeState, WeakObject, Scope
├── casting-and-serialization.md # get/as convention, string encoding, C++↔JS mapping, folly::dynamic, ISerialization
├── threading-safety.md # Single-thread rule, destruction order, hot reload pitfalls, WithRuntimeDecorator
├── calling-js-and-async.md # HostFunction install, calling JS from C++, CallInvoker, Promise patterns
├── performance.md # Batching, PropNameID cache, zero-copy ArrayBuffer, Scope in loops
├── setup-and-templates.md # Android (FBJNI), iOS (ObjC++), library scaffolding templates
├── module-approaches.md # Pure JSI vs TurboModules vs Nitro Modules decision tree
├── cpp-memory-patterns.md # unique_ptr, shared_ptr, move, RAII, circular ownership, GC boundary
└── debugging-and-pitfalls.md # Crash traces, symbolication, ASan, common JSI crash patternsAdding a Sub-skill
1. Create the directory: references/<your-topic>/ 2. Write `SKILL.md` with frontmatter:
---
name: your-topic
description: "What it covers and when to trigger. Include specific keywords users might type."
---3. Add reference files for detailed patterns and code examples 4. Register it in the parent SKILL.md sub-skills table 5. Keep SKILL.md under 500 lines. Move detailed content to reference files and link to them with a "when to read" table.
Libraries Covered
Animation Functions and Core Hooks
Gotchas, rules, and patterns for Reanimated 4 core hooks, animation functions, and modifiers.
For API signatures and config options, webfetch the linked documentation pages below.
For choosing between animation approaches (CSS vs shared value), see `animations.md`.
---
Worklet Directive
Do not add 'worklet'; to callbacks passed to Reanimated and Worklets APIs (useAnimatedStyle, useAnimatedProps, useDerivedValue, useAnimatedReaction, useAnimatedScrollHandler, useFrameCallback, gesture callbacks, animation callbacks). The Reanimated Babel plugin auto-workletizes these. The 'worklet'; directive is only needed for standalone functions you define yourself and want to run on the UI thread.
---
Core Hooks
useSharedValue
Gotchas:
- Never destructure:
const { value } = svbreaks reactivity. - For objects, reassign the entire value:
sv.value = { ...sv.value, x: 50 }. Direct mutation (sv.value.x = 50) loses reactivity. - For large arrays/objects, use
.modify()to mutate in place:sv.modify(arr => { arr.push(item); return arr; }). - Reading
.valueon the JS thread blocks until the UI thread syncs. Minimize cross-thread reads. - Never read/modify during component render. Access only in callbacks (
useAnimatedStyle, event handlers,useEffect). Reanimated will warn: _"Reading fromvalueduring component render"_ / _"Writing tovalueduring component render"_. - Use
.get()/.set()methods instead of.valuefor React Compiler compatibility. - Avoid using a shared value exclusively on the JS thread. If you only need the value on the JS thread, use
useStateinstead. Reading a shared value on the JS thread is slow (requires thread synchronization) and may return a stale value. A common mistake is reading or updating a shared value in the component body (during render):
// BAD: reading/writing shared values during render
function Counter() {
const count = useSharedValue(0);
// Triggers "Reading from value during component render" warning
const doubled = count.value * 2;
return (
<Animated.View>
{/* Triggers "Writing to value during component render" warning */}
<Button onPress={() => { count.value += 1; }} title={`Count: ${count.value}`} />
</Animated.View>
);
}
// GOOD: shared value used for UI-thread animations
function AnimatedCounter() {
const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}));
return (
<Animated.View style={animatedStyle}>
<Button onPress={() => { offset.value = withSpring(offset.value + 50); }} title="Move" />
</Animated.View>
);
}useAnimatedStyle
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: withSpring(offset.value) }],
}));
<Animated.View style={[styles.box, animatedStyle]} />Rules:
- Keep static styles in
StyleSheet.create(). Only put dynamic parts inuseAnimatedStyle. - Animated styles override static styles in the style array.
- Removing an animated style does not unset its values. Manually set properties to
undefinedto clear them. - Never mutate shared values inside the updater (e.g.,
sv.value = withTiming(1)in the callback). This causes infinite loops. - The callback runs on the JS thread first, then immediately on the UI thread. Use
global._WORKLETto guard thread-specific code.
useAnimatedProps
For animating component properties (not styles). Do all value conversions directly inside the useAnimatedProps callback instead of using adapters like SVGAdapter:
const animatedProps = useAnimatedProps(() => ({
cx: x.value,
r: radius.value,
}));Custom color properties require manual processColor() wrapping inside the callback.
useDerivedValue
Creates a read-only shared value that recomputes when its dependencies change. Runs on the UI thread automatically. The .set() method is deprecated and will be removed.
If you need access to the previous value, use useAnimatedReaction instead.
createAnimatedComponent
Function components can accept a ref prop directly (React 19+). For older React versions, wrap with React.forwardRef(). Class components work directly.
Built-in animated components: Animated.View, Animated.Text, Animated.Image, Animated.ScrollView, Animated.FlatList.
useAnimatedRef
The ref value (current) is null until the component mounts. It is only accessible from the JS thread, so do not read it inside worklets.
---
Animation Functions
withSpring
Two configuration modes (cannot mix):
- Physics-based (stiffness/damping)
- Duration-based (duration/dampingRatio)
dampingRatio values: < 1 = underdamped (bouncy), 1 = critically damped (no bounce, fastest settle), > 1 = overdamped (slow, no bounce).
If both physics-based and duration-based configs are provided, duration-based overrides.
withDecay
clamp is required when rubberBandEffect is true. The rubber band effect makes the animation bounce at clamp boundaries instead of stopping.
---
Animation Modifiers
withRepeat
- Non-positive values (
0,-1) repeat infinitely until cancelled or unmounted. reverse: truecreates a ping-pong effect (plays forward, then backward).- `reverse` only works with animation functions (
withSpring,withTiming). It does not work with animation modifiers likewithSequence.
withClamp
Limits the animated value range. Designed for withSpring to prevent overshoot beyond boundaries. When the spring hits a clamped boundary, its dampingRatio is automatically reduced.
---
Composition Patterns
Modifiers nest freely:
// Staggered entrance
items.forEach((_, i) => {
sv[i].value = withDelay(i * 100, withSpring(1));
});
// Infinite ping-pong
sv.value = withRepeat(withTiming(1, { duration: 800 }), -1, true);
// Multi-step sequence
sv.value = withSequence(
withTiming(50, { duration: 200 }),
withSpring(0),
withDelay(300, withTiming(100))
);
// Clamped spring
sv.value = withClamp({ min: 0, max: 200 }, withSpring(scrollTarget));Callback behavior
Callbacks on withTiming, withSpring, withDecay, and withRepeat are automatically workletized and run on the UI thread. They receive (finished: boolean, current: AnimatableValue) where finished is true if the animation completed normally, false if cancelled.
Animations Performance and Accessibility
Reanimated 4 requires the New Architecture (Fabric). All guidance here assumes that.
---
120fps Support
Enable ProMotion display support on iOS by adding to Info.plist:
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>Without this flag, iOS caps animations at 60fps even on ProMotion devices.
---
Feature Flags
Reanimated 4 exposes feature flags to opt into fixes for known New Architecture issues. Enable them early in your app entry point, before any Reanimated code runs.
Flickering / Jittering While Scrolling
Animated components like sticky headers flicker during FlatList or ScrollView scrolling on the New Architecture.
Fix: Upgrade to React Native 0.81+ and enable:
preventShadowTreeCommitExhaustion(experimental release-level flag in RN)DISABLE_COMMIT_PAUSING_MECHANISM(Reanimated feature flag)
FPS Drops During Scrolling
FPS drops when many animated components are visible during scroll.
Fix: Upgrade to React Native 0.80+ and Reanimated 4.2.0+, then enable:
USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS
Alternative: Enable enableCppPropsIteratorSetter (experimental).
Low FPS with Many Simultaneous Animations
Fix: Enable platform-specific synchronous UI update flags:
ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS(available since 4.0.0)IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS(available since 4.2.0)
Note: these flags may interfere with touch detection on animated transform elements. Prefer Pressable from react-native-gesture-handler over the core Pressable when using these flags.
---
Simultaneous Animation Limits
Reanimated can handle many animated components, but performance degrades at scale:
| Platform | Practical limit |
|---|---|
| iOS | ~500 components |
| Low-end Android | ~100 components |
For lists with many animated items, consider reducing animation complexity on low-end devices using useReducedMotion. For highly complex animation scenes (hundreds of elements), consider Reanimated + react-native-skia instead of animating native views.
---
Prefer Non-Layout Properties
Animating layout properties (top, left, width, height, margin, padding) forces a layout pass on every frame.
Prefer properties that use the fast path:
transform(translateX,translateY,scale,rotate)opacitybackgroundColor
If a design requires a size change, consider scale transforms for the same visual effect without triggering layout.
---
Avoid Reading Shared Values on the JS Thread
Reading sv.value inside React render, event handlers, or useEffect triggers a synchronization from the UI thread to the JS thread, which can block the JS thread.
Instead, use useDerivedValue to transform shared values and useAnimatedStyle to consume them — both run on the UI thread.
---
Memoize Callbacks and Gesture Objects
Frame callbacks and gesture objects are re-created on every render by default. Wrap them:
const frameCallback = useFrameCallback(
useCallback((frameInfo) => {
// runs on UI thread every frame
}, [])
);
const gesture = useMemo(() =>
Gesture.Pan().onUpdate((e) => {
offset.value = e.translationX;
}),
[]
);If React Compiler is available, it handles memoization automatically.
---
Worklet Closure Optimization
Worklets capture variables from their surrounding scope. Capturing large objects causes performance issues due to serialization overhead.
// Bad: captures entire theme object
const theme = useTheme();
const style = useAnimatedStyle(() => ({
backgroundColor: theme.colors.primary,
}));
// Good: extract only what is needed
const primaryColor = useTheme().colors.primary;
const style = useAnimatedStyle(() => ({
backgroundColor: primaryColor,
}));Extract specific properties before the worklet to minimize the closure payload.
Functions marked with 'worklet' are not hoisted. They must be defined before they are referenced in other worklets.
---
Debug vs. Release Builds
Always profile animations in a release build. Debug builds add significant JS overhead (Metro bundler, Hermes debug mode, dev warnings) that makes animations appear slower than they are in production.
npx react-native run-android --mode=releaseOn Android, use debugOptimized build variant for a better dev experience with closer-to-production performance.
---
Accessibility
useReducedMotion
const reduceMotion = useReducedMotion();Returns true if the device has reduced motion enabled at app start. Does not update at runtime if the user changes the setting.
Use it to conditionally render animations or pick simpler alternatives:
<Animated.View entering={reduceMotion ? undefined : FadeIn} />ReducedMotionConfig
Sets global animation behavior for the entire app:
import { ReducedMotionConfig, ReduceMotion } from 'react-native-reanimated';
// Place near app root
<ReducedMotionConfig mode={ReduceMotion.System} />Modes:
ReduceMotion.System(default) — follow device settingReduceMotion.Always— always disable animationsReduceMotion.Never— always enable animations
Behavior per animation type when reduced motion is enabled
| Animation | Behavior |
|---|---|
withSpring, withTiming | Jump to toValue immediately |
withDecay | Return current value (respecting clamp) |
withDelay | Start next animation immediately |
withRepeat (infinite or even + reversed) | Do not start |
withRepeat (other) | Run once |
withSequence | Only start children with reduceMotion: Never |
| Entering / keyframe / layout animations | Jump to endpoint immediately |
| Exiting / shared element transitions | Omitted entirely |
Higher-order animations pass their reduceMotion config to children unless a child has its own explicit config.
Animations
Production-quality animation patterns for React Native apps using Reanimated 4 on the New Architecture.
For animation function APIs and core hooks, see `animation-functions.md`. For entering/exiting and layout transition animations, see `layout-animations.md`. For scroll-driven animations and event-based patterns, see `scroll-and-events.md`. For canvas animations with Skia (high element counts, sprites, path morphing), see `canvas-animations.md`. For GPU shader animations (particles, noise, SDF, physics, 3D), see `gpu-animations.md`. For performance tuning and feature flags, see `animations-performance.md`.
---
Decision Tree
Pick the animation type based on what drives the animation and what it needs to compute.
Does the effect require per-pixel GPU computation?
(Particle systems, fluid/physics sims, procedural noise, SDF shapes, 3D scenes)
├── YES → Use GPU Shaders (react-native-wgpu + TypeGPU) → see gpu-animations.md
└── NO → Does it animate more than ~100 elements (low-end Android) or ~500 (iOS)?
├── YES → Use Reanimated + react-native-skia → see canvas-animations.md
└── NO → Is the animation driven by a state change (not a gesture or continuous input)?
├── YES → Can it be expressed as a simple A→B property transition?
│ ├── YES → Use CSS Transition (transitionProperty)
│ └── NO → Does it need a defined keyframe sequence?
│ ├── YES → Use CSS Animation (animationName + keyframes)
│ └── NO → Use CSS Transition with multiple properties
└── NO → Is it gesture-driven, or does it need math / trig / layout reads?
├── Simple feedback (press/release, toggle)?
│ └── YES → Use CSS Transition + Pressable + React state
└── Continuous tracking, math, or layout reads?
└── YES → Use Shared Value Animation (useSharedValue + useAnimatedStyle)Default to CSS transitions and CSS animations. They are declarative, easier to read, and remove the overhead of worklet execution. This includes simple gesture feedback like button presses: use CSS transitions with Pressable + React state instead of shared values to avoid worklets and thread bridging. Reach for shared values when the animation requires continuous tracking (pan, pinch, scroll), per-frame math, or layout reads. When the scene animates more than ~100 elements on low-end Android or ~500 on iOS, switch to Reanimated + react-native-skia, which renders to a single canvas and avoids per-view overhead. Reach for GPU shaders (react-native-wgpu + TypeGPU) when the animation involves per-pixel computation, physics simulations, or 3D rendering that operates outside the React Native view hierarchy.
---
CSS Transitions
Use when a component's style should animate smoothly whenever a state-driven prop changes. For the full property list and timing functions, webfetch the CSS Transitions docs.
<Animated.View
style={{
width: isExpanded ? 200 : 100,
transitionProperty: 'width',
transitionDuration: 300,
transitionTimingFunction: 'ease-out',
}}
/>When using arrays, the order must match the transitionProperty array:
transitionProperty: ['width', 'opacity', 'backgroundColor'],
transitionDuration: [300, 200, 150],
transitionTimingFunction: ['ease-out', 'linear', 'ease-in-out'],CSS Transitions for simple gesture feedback
For simple press/release or toggle animations, CSS transitions paired with Pressable and React state avoid the need for shared values, worklets, and scheduleOnRN thread bridging. The animation stays declarative and runs entirely through Reanimated's CSS transition engine.
import { useState } from 'react';
import { Pressable } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
function PressableButton({ label, onPress }) {
const [pressed, setPressed] = useState(false);
return (
<Pressable
onPress={onPress}
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}>
<Animated.View
style={{
transform: pressed
? [{ scale: 0.96 }, { translateY: 4 }]
: [{ scale: 1 }, { translateY: 0 }],
boxShadow: pressed
? '0px 1px 2px rgba(0, 0, 0, 0.3)'
: '0px 6px 10px rgba(0, 0, 0, 0.3)',
transitionProperty: ['transform', 'boxShadow'],
transitionDuration: '80ms',
}}>
<Text>{label}</Text>
</Animated.View>
</Pressable>
);
}Reserve shared value animations for continuous gesture tracking (pan, pinch, scroll-driven) where the animation must follow finger position on every frame without a JS thread round-trip.
Discrete properties
Properties like flexDirection, justifyContent, and alignItems cannot be smoothly animated. By default, they change instantly. To make them flip at the animation midpoint, set:
transitionBehavior: 'allow-discrete',The display property flips at animation start (0%) instead of the midpoint. For smoother transitions of discrete properties, use Layout Animations instead.
Rules
- Avoid
transitionProperty: 'all'— it forces evaluation of every style property on each frame and degrades performance. - Negative delays start the transition partway through (e.g.,
'-5s'on a 10s transition starts at 50%). - CSS transitions cannot animate discrete properties smoothly without
transitionBehavior: 'allow-discrete'.
---
CSS Animations
Use when the animation follows a predefined keyframe sequence independent of external state — loaders, pulse effects, entrance choreography. For the full property list, webfetch the CSS Animations docs.
const pulse = {
'0%': { opacity: 1 },
'50%': { opacity: 0.4 },
'100%': { opacity: 1 },
};
<Animated.View
style={{
animationName: pulse,
animationDuration: '1200ms',
animationIterationCount: 'infinite',
animationTimingFunction: 'ease-in-out',
}}
/>Reanimated uses the current element state as the implicit 0% keyframe, so you only need to define the frames that differ. At minimum, one keyframe is required.
Multiple animations
const fadeInOut = { '0%': { opacity: 0 }, '100%': { opacity: 1 } };
const moveLeft = { '100%': { transform: [{ translateX: -100 }] } };
<Animated.View
style={{
animationName: [fadeInOut, moveLeft],
animationDuration: ['2.5s', '5s'],
animationIterationCount: ['infinite', 1],
}}
/>If multiple animations target the same property, the later animation in the array wins.
Rules
- The timing function on the last keyframe (
100%,to, or1) is ignored — there is no subsequent keyframe to animate toward. - All properties in the
transformarray must appear in the same order across all keyframes. - Avoid
animationFillMode: 'forwards'or'both'with fractionalanimationIterationCountand relative units (percentages). If the parent resizes after the animation, the child retains stale dimensions. - For infinite CSS animations, set
animationIterationCount: 'infinite'. The animation stops automatically on unmount — no manual cleanup needed. - Negative delays start the animation partway through its cycle.
- Pause and resume with
animationPlayState: 'paused'/'running'.
---
Shared Value Animations
Use when:
- The animation is driven by a gesture or continuous input (scroll position, drag offset)
- It requires math, trigonometric functions, or interpolation between computed values
- It needs to read layout measurements on each frame (
measure,useAnimatedRef) - Multiple animated values need to be derived from a single source of truth
const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: withSpring(offset.value) }],
}));
// Gesture-driven example
const gesture = Gesture.Pan().onUpdate((e) => {
offset.value = e.translationX;
});Avoid reading sharedValue.value on the JS thread inside React render or event handlers — it causes a synchronization that blocks the JS thread. Derive values from shared values using useDerivedValue instead.
---
Animating Text
Avoid updating Animated.Text content by changing state — it triggers a full React re-render for every frame.
For animated numeric counters or any frequently-changing text, use AnimatedTextInput with animatedProps:
import Animated, { useAnimatedProps } from 'react-native-reanimated';
import { TextInput } from 'react-native';
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
function Counter({ progress }: { progress: SharedValue<number> }) {
const animatedProps = useAnimatedProps(() => ({
text: String(Math.round(progress.value)),
defaultValue: '0',
}));
return (
<AnimatedTextInput
animatedProps={animatedProps}
editable={false}
style={styles.counter}
/>
);
}This updates the native text node directly on the UI thread, bypassing React and eliminating re-renders.
---
Infinite Animations
CSS animations with animationIterationCount: 'infinite' clean up automatically on unmount.
For shared value infinite animations, always cancel them in the useEffect cleanup:
useEffect(() => {
offset.value = withRepeat(withTiming(1, { duration: 800 }), -1, true);
return () => {
cancelAnimation(offset);
};
}, []);Never start infinite animations outside the component lifecycle (module scope, global timers). They cannot be cleaned up and will leak.
---
Prefer Non-Layout Properties
Animating layout properties (top, left, width, height, margin, padding) forces a layout pass on every frame, which is expensive and causes jank.
Prefer:
transform: [{ translateX }, { translateY }, { scale }, { rotate }]opacitybackgroundColor
If a design requires what looks like a size change, consider scale transforms — same visual effect without triggering layout.
---
Supported Style Properties
Most React Native style properties are animatable. Key exceptions and platform notes:
- `flexBasis`: Changes are calculated but never applied to the view. Use
flexGrow/flexShrinkinstead. - Shadow properties:
shadowOffset,shadowOpacity,shadowRadiusdo not work on Android. UseboxShadowinstead (works on all platforms). - Web shadows: All shadow styles must be specified in every keyframe on Web, or they are lost.
- `tintColor` on iOS: Must be present in the initial style when the
Imagecomponent mounts. Adding it later has no effect. - Style inheritance: Not supported. Properties that normally inherit in CSS (e.g.,
textDecorationColorfromcolor) must be set explicitly. - Mixed-unit margins: Interpolating between absolute and percentage margins may produce unexpected results when the parent's dimensions are affected by the child's margins.
---
Threading: scheduleOnRN instead of runOnJS
runOnJS is removed in Reanimated 4. Use scheduleOnRN to call JS-thread functions from a worklet. Arguments are passed directly, not curried:
// Reanimated 3 (removed)
runOnJS(setCount)(newCount);
// Reanimated 4
scheduleOnRN(setCount, newCount);scheduleOnRN schedules the call asynchronously on the React Native runtime. Functions passed to scheduleOnRN must be defined in JS thread scope (they cannot be created inside worklets or animation callbacks).
Canvas Animations with Skia
Canvas-based animations using @shopify/react-native-skia with Reanimated. Use when the scene animates more elements than Reanimated can handle with native views (~100 on low-end Android, ~500 on iOS), or when drawing custom 2D graphics (charts, graphs, generative art, sprite sheets) that benefit from rendering to a single canvas surface.
Skia renders everything to one canvas view, avoiding per-view overhead. All elements share the same draw call pipeline, making it efficient for hundreds of animated shapes, paths, or sprites.
For full API details, webfetch the official documentation.
Version requirements: react-native@>=0.79, react@>=19. For react-native@<=0.78, use @shopify/react-native-skia@1.12.4 or below.
npm install @shopify/react-native-skiaWith Expo:
npx expo install @shopify/react-native-skiaBundle size impact: ~6 MB on iOS, ~4 MB on Android, ~2.9 MB on Web.
---
Reanimated Integration
Skia components accept Reanimated shared values and derived values as props directly. There is no need for createAnimatedComponent or useAnimatedProps.
import { useEffect } from 'react';
import { Canvas, Circle, Group } from '@shopify/react-native-skia';
import {
useDerivedValue,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated';
export const AnimatedCircles = () => {
const size = 256;
const r = useSharedValue(0);
const c = useDerivedValue(() => size - r.value);
useEffect(() => {
r.value = withRepeat(withTiming(size * 0.33, { duration: 1000 }), -1);
}, [r, size]);
return (
<Canvas style={{ flex: 1 }}>
<Group blendMode="multiply">
<Circle cx={r} cy={r} r={r} color="cyan" />
<Circle cx={c} cy={r} r={r} color="magenta" />
<Circle cx={size / 2} cy={c} r={r} color="yellow" />
</Group>
</Canvas>
);
};Canvas Size on the UI Thread
Use the onSize prop to get the canvas dimensions as a shared value, which updates whenever the canvas resizes:
import { useSharedValue, useDerivedValue } from 'react-native-reanimated';
import { Canvas, Rect } from '@shopify/react-native-skia';
const Demo = () => {
const size = useSharedValue({ width: 0, height: 0 });
const rect = useDerivedValue(() => ({
x: 0,
y: 0,
width: size.value.width,
height: size.value.height,
}));
return (
<Canvas style={{ flex: 1 }} onSize={size}>
<Rect color="cyan" rect={rect} />
</Canvas>
);
};---
Color Interpolation
Skia uses a different color storage format from Reanimated. interpolateColor from Reanimated will produce incorrect results. Use interpolateColors from @shopify/react-native-skia instead:
import {
Canvas,
LinearGradient,
Fill,
interpolateColors,
vec,
} from '@shopify/react-native-skia';
import {
useDerivedValue,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated';
import { useEffect } from 'react';
import { useWindowDimensions } from 'react-native';
const startColors = ['rgba(34,193,195,0.4)', 'rgba(63,94,251,1)'];
const endColors = ['rgba(0,212,255,0.4)', 'rgba(252,70,107,1)'];
export const AnimatedGradient = () => {
const { width, height } = useWindowDimensions();
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withRepeat(
withTiming(startColors.length - 1, { duration: 4000 }),
-1,
true
);
}, []);
const gradientColors = useDerivedValue(() => [
interpolateColors(progress.value, [0, 1], startColors),
interpolateColors(progress.value, [0, 1], endColors),
]);
return (
<Canvas style={{ flex: 1 }}>
<Fill>
<LinearGradient
start={vec(0, 0)}
end={vec(width, height)}
colors={gradientColors}
/>
</Fill>
</Canvas>
);
};---
Rendering Modes
Skia supports two rendering paradigms. Both use the same <Canvas> element and can be combined in a single scene.
Retained Mode (default)
Declare the scene as a React component tree. Skia converts it into a display list that is efficient to animate with Reanimated. Animating property values has near-zero FFI cost because the display list structure stays the same.
Best for: UI animations, data visualizations, fixed-structure scenes.
Immediate Mode (Picture API)
Issue drawing commands directly to a canvas on every frame. Use when the number of drawing commands changes dynamically (the number of elements is itself animated).
Best for: games, generative art, particle trails, any scene where entities are created/destroyed per frame.
import { Canvas, Picture, Skia } from '@shopify/react-native-skia';
import {
useDerivedValue,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated';
import { useEffect } from 'react';
const size = 256;
const paint = Skia.Paint();
const recorder = Skia.PictureRecorder();
export const CircleTrail = () => {
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withRepeat(withTiming(1, { duration: 3000 }), -1, true);
}, [progress]);
const picture = useDerivedValue(() => {
'worklet';
const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, size, size));
const count = Math.floor(progress.value * 20);
for (let i = 0; i < count; i++) {
const r = ((i + 1) / 20) * (size / 2);
paint.setColor(Skia.Color(`rgba(0, 122, 255, ${(i + 1) / 20})`));
canvas.drawCircle(size / 2, size / 2, r, paint);
}
return recorder.finishRecordingAsPicture();
});
return (
<Canvas style={{ flex: 1 }}>
<Picture picture={picture} />
</Canvas>
);
};Picture does not inherit Group paint properties. Apply effects using the layer property on a parent Group instead.
---
Atlas: Batched Sprite and Tile Animation
For Atlas batched sprite and tile animations (useTexture, useRSXformBuffer, RSXform matrix format), read canvas-atlas.md.
---
Path Animations
Skia provides hooks for efficient path animation on the UI thread. For full API details, webfetch the hooks documentation.
usePathInterpolation
Interpolates between path shapes based on a progress value. All paths must contain the same number and types of commands for proper interpolation. For paths with different structures, use the flubber library to generate compatible intermediate paths.
import { useEffect } from 'react';
import { useSharedValue, withTiming } from 'react-native-reanimated';
import { Skia, usePathInterpolation, Canvas, Path } from '@shopify/react-native-skia';
const angry = Skia.Path.MakeFromSVGString('M 16 25 C 32 27 ...')!;
const normal = Skia.Path.MakeFromSVGString('M 21 31 C 31 32 ...')!;
const happy = Skia.Path.MakeFromSVGString('M 21 45 C 21 37 ...')!;
const MorphingFace = () => {
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withTiming(1, { duration: 1000 });
}, []);
const path = usePathInterpolation(progress, [0, 0.5, 1], [angry, normal, happy]);
return (
<Canvas style={{ flex: 1 }}>
<Path path={path} style="stroke" strokeWidth={5} strokeCap="round" strokeJoin="round" />
</Canvas>
);
};usePathValue
Animates a path using imperative commands inside a worklet. Supports 3D transforms via processTransform3d:
import { useSharedValue, withSpring } from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { usePathValue, Canvas, Path, processTransform3d, Skia } from '@shopify/react-native-skia';
const rrct = Skia.Path.Make();
rrct.addRRect(Skia.RRectXY(Skia.XYWHRect(0, 0, 100, 100), 10, 10));
export const Card3D = () => {
const rotateY = useSharedValue(0);
const gesture = Gesture.Pan().onChange((e) => {
rotateY.value -= e.changeX / 300;
});
const clip = usePathValue((path) => {
'worklet';
path.transform(
processTransform3d([
{ translate: [50, 50] },
{ perspective: 300 },
{ rotateY: rotateY.value },
{ translate: [-50, -50] },
])
);
}, rrct);
return (
<GestureDetector gesture={gesture}>
<Canvas style={{ flex: 1 }}>
<Path path={clip} />
</Canvas>
</GestureDetector>
);
};useClock
Returns a continuously incrementing shared value (milliseconds since activation). Useful for parametric/time-based animations:
import { Canvas, useClock, vec, Circle } from '@shopify/react-native-skia';
import { useDerivedValue } from 'react-native-reanimated';
export default function Lissajous() {
const t = useClock();
const transform = useDerivedValue(() => {
const scale = (2 / (3 - Math.cos(2 * t.value))) * 200;
return [
{ translateX: scale * Math.cos(t.value) },
{ translateY: scale * (Math.sin(2 * t.value) / 2) },
];
});
return (
<Canvas style={{ flex: 1 }}>
<Circle c={vec(0, 0)} r={50} color="cyan" transform={transform} />
</Canvas>
);
}---
Gesture Integration
Wrap the Canvas with GestureDetector from react-native-gesture-handler. Shared values updated in gesture callbacks drive Skia props on the UI thread:
import { Canvas, Circle, Fill } from '@shopify/react-native-skia';
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import { useSharedValue, withDecay } from 'react-native-reanimated';
import { useWindowDimensions } from 'react-native';
export const DraggableCircle = () => {
const { width } = useWindowDimensions();
const translateX = useSharedValue(width / 2);
const gesture = Gesture.Pan()
.onChange((e) => {
translateX.value += e.changeX;
})
.onEnd((e) => {
translateX.value = withDecay({
velocity: e.velocityX,
clamp: [0, width],
});
});
return (
<GestureDetector gesture={gesture}>
<Canvas style={{ flex: 1 }}>
<Fill color="white" />
<Circle cx={translateX} cy={40} r={20} color="#3E3E" />
</Canvas>
</GestureDetector>
);
};Element Tracking
Gestures apply to the entire canvas by default. To target a specific drawn element, overlay an invisible Animated.View that mirrors the element's transforms and attach the gesture to that view:
import { View } from 'react-native';
import { Canvas, Circle, Fill } from '@shopify/react-native-skia';
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated';
const radius = 30;
export const TrackedCircle = () => {
const x = useSharedValue(100);
const y = useSharedValue(100);
const overlayStyle = useAnimatedStyle(() => ({
position: 'absolute',
top: -radius,
left: -radius,
width: radius * 2,
height: radius * 2,
transform: [{ translateX: x.value }, { translateY: y.value }],
}));
const gesture = Gesture.Pan().onChange((e) => {
x.value += e.x;
y.value += e.y;
});
return (
<View style={{ flex: 1 }}>
<Canvas style={{ flex: 1 }}>
<Fill color="white" />
<Circle cx={x} cy={y} r={radius} color="cyan" />
</Canvas>
<GestureDetector gesture={gesture}>
<Animated.View style={overlayStyle} />
</GestureDetector>
</View>
);
};---
SKSL Runtime Shaders
Skia provides a shading language (SKSL) similar to GLSL for per-pixel effects. Compile shaders with Skia.RuntimeEffect.Make and apply them as children of drawing elements or as image filters.
For full SKSL syntax, webfetch the shading language docs.
import { Canvas, Skia, Shader, Fill } from '@shopify/react-native-skia';
const source = Skia.RuntimeEffect.Make(`
uniform vec2 resolution;
uniform float time;
vec4 main(vec2 pos) {
vec2 uv = pos / resolution;
float d = length(uv - 0.5);
float pulse = 0.5 + 0.5 * sin(d * 20.0 - time * 3.0);
return vec4(uv.x, pulse, uv.y, 1.0);
}
`)!;
// Pass shared values as uniforms to animate on the UI threadRuntimeShader Image Filter
Apply SKSL shaders as image filters to existing drawings. The currently filtered image is passed as the image uniform:
import { Canvas, Skia, Group, Circle, RuntimeShader } from '@shopify/react-native-skia';
const source = Skia.RuntimeEffect.Make(`
uniform shader image;
half4 main(float2 xy) {
return image.eval(xy).rbga;
}
`)!;
export const FilteredCircle = () => (
<Canvas style={{ flex: 1 }}>
<Group>
<RuntimeShader source={source} />
<Circle cx={128} cy={128} r={128} color="lightblue" />
</Group>
</Canvas>
);RuntimeShader does not account for pixel density scaling. For crisp output, apply supersampling: scale up by PixelRatio.get() before filtering, then scale back down. See the RuntimeShader docs for details.
---
Textures
Create GPU textures on the UI thread for use with Atlas or Image components.
- `useTexture`: Creates a texture from React elements. Returns a shared value.
- `useImageAsTexture`: Uploads an image from a source to the GPU.
- `usePictureAsTexture`: Creates a texture from an
SkPicture(imperative API).
For full API, webfetch the textures docs.
---
Rules
- Use
interpolateColorsfrom@shopify/react-native-skiafor color animation.interpolateColorfrom Reanimated uses a different color format and produces incorrect results in Skia. - All paths passed to
usePathInterpolationmust have the same number and types of commands. Mismatched paths produce undefined interpolation behavior. PictureandSVGcomponents do not inheritGrouppaint properties. Use thelayerproperty to apply effects.- The transform origin in Skia
Groupis the top-left corner, not the center (unlike React Native views). Use theoriginprop to adjust. - All rotations in Skia are in radians.
Skia.RuntimeEffect.Makereturnsnullif the shader fails to compile. Always check the result.- Canvas accessibility: the
Canvassupports the same accessibility properties as aView. Make drawn elements accessible by overlaying views on top of the canvas (same pattern as element tracking for gestures).
Atlas: Batched Sprite and Tile Animation
The Atlas component renders many instances of the same texture in a single draw call with per-instance transforms. Use it for tile maps, sprite animations, and any scene with hundreds of similar objects.
Combine with useTexture (creates the texture on the UI thread) and useRSXformBuffer (efficiently animates per-sprite transforms in a worklet):
import {
Skia,
Canvas,
Atlas,
Rect,
Group,
rect,
useTexture,
useRSXformBuffer,
} from '@shopify/react-native-skia';
import { useSharedValue } from 'react-native-reanimated';
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
const size = { width: 25, height: 11.25 };
const strokeWidth = 2;
const textureSize = {
width: size.width + strokeWidth,
height: size.height + strokeWidth,
};
export const SpriteGrid = () => {
const pos = useSharedValue({ x: 0, y: 0 });
const texture = useTexture(
<Group>
<Rect
rect={rect(strokeWidth / 2, strokeWidth / 2, size.width, size.height)}
color="cyan"
/>
<Rect
rect={rect(strokeWidth / 2, strokeWidth / 2, size.width, size.height)}
color="blue"
style="stroke"
strokeWidth={strokeWidth}
/>
</Group>,
textureSize
);
const gesture = Gesture.Pan().onChange((e) => (pos.value = e));
const count = 150;
const gridWidth = 256;
const sprites = new Array(count)
.fill(0)
.map(() => rect(0, 0, textureSize.width, textureSize.height));
const transforms = useRSXformBuffer(count, (val, i) => {
'worklet';
const tx = 5 + ((i * size.width) % gridWidth);
const ty = 25 + Math.floor(i / (gridWidth / size.width)) * size.width;
const r = Math.atan2(pos.value.y - ty, pos.value.x - tx);
val.set(Math.cos(r), Math.sin(r), tx, ty);
});
return (
<GestureDetector gesture={gesture}>
<Canvas style={{ flex: 1 }}>
<Atlas image={texture} sprites={sprites} transforms={transforms} />
</Canvas>
</GestureDetector>
);
};RSXform
Atlas transforms use a compressed rotation-scale-translate matrix [scos, ssin, tx, ty]:
// Identity (no transform)
Skia.RSXform(1, 0, 0, 0);
// Scale by 2, translate to (50, 100)
Skia.RSXform(2, 0, 50, 100);
// Rotate by PI/4, translate to (50, 100)
const r = Math.PI / 4;
Skia.RSXform(Math.cos(r), Math.sin(r), 50, 100);
// Scale by 2, rotate by PI/4 with pivot (25, 25)
Skia.RSXformFromRadians(2, r, 0, 0, 25, 25);GPU Shader Animations
GPU-accelerated animations using WebGPU shaders in React Native. Use when animation requirements exceed what Reanimated can handle: massive particle counts, fluid/physics simulations, procedural noise, custom shader effects, SDF rendering, or 3D scenes.
Requires React Native 0.81+ and the New Architecture.
Version requirements: react-native-wgpu requires react-native-reanimated >= 4.2.1 and react-native-worklets >= 0.7.2. Upgrade both before installing if the project uses older versions:
npm install react-native-reanimated@latest react-native-worklets@latest
npm install react-native-wgpu---
When to Use GPU Shaders
GPU shaders are the right choice when:
- The effect requires per-pixel computation (noise fields, distortion, blur, glow)
- Physics simulations drive the animation (fluid, cloth, boids flocking, gravity)
- You need signed distance function (SDF) rendering for procedural shapes
- 3D rendering is required (meshes, lighting, skeletal animation via Three.js)
- Animation state is computed from math that benefits from GPU parallelism
For high element counts without per-pixel computation (hundreds of 2D shapes, sprites, tiles), prefer react-native-skia with the Atlas API instead (see canvas-animations.md). Skia renders to a single canvas with lower setup cost than WebGPU.
Stick with Reanimated when animating standard UI components (opacity, transforms, layout changes) or responding to gestures. GPU shaders render into a Canvas element, separate from the React Native view hierarchy.
---
Setup
react-native-wgpu
Provides the WebGPU API in React Native using Dawn as the backend.
npm install react-native-wgpuWith Expo:
npx create-expo-app@latest -e with-webgpuTypeGPU (recommended)
Type-safe abstraction over WebGPU. Write shader logic in TypeScript that compiles to WGSL.
npm install typegpu
npm install --save-dev @webgpu/types unplugin-typegpuAdd @webgpu/types to tsconfig.json:
{ "compilerOptions": { "types": ["@webgpu/types"] } }Add the Babel plugin for 'use gpu' function syntax:
// babel.config.js
module.exports = (api) => {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['unplugin-typegpu/babel'],
};
};Clear Metro cache after adding the plugin: npx expo --clear.
Expo Go is not supported. Run npx expo prebuild if migrating from Expo Go.
On the iOS simulator, disable Metal Validation in Edit Scheme to avoid crashes.
---
Canvas and Device Setup
The Canvas component provides the WebGPU surface. Use useDevice and useCanvasRef hooks from react-native-wgpu:
import { Canvas, useDevice, useCanvasRef } from 'react-native-wgpu';
import tgpu, { d } from 'typegpu';
function GPUScene() {
const { device = null } = useDevice();
const ref = useCanvasRef();
useEffect(() => {
if (!device) return;
const context = ref.current!.getContext('webgpu')!;
const root = tgpu.initFromDevice({ device });
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: 'premultiplied' });
// Create pipelines and render here
// ...
context.present(); // Required: manually present the frame
return () => root.destroy();
}, [device]);
return <Canvas ref={ref} style={{ flex: 1 }} />;
}Key differences from Web WebGPU
- `context.present()` is required after submitting commands. React Native does not auto-present frames.
- Canvas size uses device pixel ratio:
canvas.width = canvas.clientWidth * PixelRatio.get(). - On Android,
alphaModeis ignored incontext.configure(). Use thetransparentprop onCanvasinstead.
---
Reanimated Integration
WebGPU objects (GPUDevice, GPUCanvasContext) are automatically registered for worklet serialization. You can pass them directly to worklets and run GPU rendering on the UI thread:
import { scheduleOnUI } from 'react-native-worklets';
const renderFrame = (device: GPUDevice, context: GPUCanvasContext) => {
'worklet';
const commandEncoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const pass = commandEncoder.beginRenderPass({
colorAttachments: [{
view: textureView,
clearValue: [0, 0, 0, 1],
loadOp: 'clear',
storeOp: 'store',
}],
});
pass.setPipeline(pipeline);
pass.draw(3);
pass.end();
device.queue.submit([commandEncoder.finish()]);
context.present();
};
// Trigger from RN thread
scheduleOnUI(renderFrame, device, context);This requires react-native-reanimated and react-native-worklets as peer dependencies:
npm install react-native-reanimated react-native-workletsUse this pattern to tie GPU rendering to Reanimated shared values or useFrameCallback for continuous animation loops.
---
Render Pipelines with TypeGPU
TypeGPU provides a type-safe API for creating GPU pipelines with TypeScript shader functions.
Vertex and fragment functions
import tgpu, { d } from 'typegpu';
const positions = tgpu.const(d.arrayOf(d.vec2f, 3), [
d.vec2f(0.0, 0.5),
d.vec2f(-0.5, -0.5),
d.vec2f(0.5, -0.5),
]);
const mainVertex = tgpu.vertexFn({
in: { vertexIndex: d.builtin.vertexIndex },
out: { pos: d.builtin.position, uv: d.vec2f },
})(({ vertexIndex }) => {
'use gpu';
return {
pos: d.vec4f(positions.$[vertexIndex], 0, 1),
uv: positions.$[vertexIndex],
};
});
const mainFragment = tgpu.fragmentFn({
in: { uv: d.vec2f },
out: d.vec4f,
})(({ uv }) => {
'use gpu';
return d.vec4f(uv.x, uv.y, 0.5, 1);
});Creating and executing the pipeline
const pipeline = root.createRenderPipeline({
vertex: mainVertex,
fragment: mainFragment,
targets: { format: navigator.gpu.getPreferredCanvasFormat() },
});
pipeline
.withColorAttachment({ view: context })
.draw(3);
context.present();---
Compute Pipelines
Use compute shaders when the animation state (positions, velocities, colors) is computed on the GPU. Write the state to a storage buffer, then read it in a render pipeline.
Standard compute pipeline
const particleBuffer = root.createMutable(
d.arrayOf(d.vec2f, 1000),
initialPositions,
);
const updateParticles = tgpu.computeFn({
workgroupSize: [64],
in: { gid: d.builtin.globalInvocationId },
})(({ gid }) => {
'use gpu';
const idx = gid.x;
particleBuffer.$[idx] = particleBuffer.$[idx].add(d.vec2f(0.001, 0));
});
root.createComputePipeline({ compute: updateParticles })
.dispatchWorkgroups(16); // 16 workgroups * 64 threads = 1024 particlesGuarded compute pipeline (simplified)
For simple parallel loops without manual workgroup sizing:
const data = root.createMutable(d.arrayOf(d.f32, 512));
root.createGuardedComputePipeline((x) => {
'use gpu';
data.$[x] = data.$[x] * 2;
}).dispatchThreads(512);Think of dispatchThreads(n) as a parallelized for (let i = 0; i < n; i++) that runs on the GPU.
---
Common Animation Patterns
Particle system
Define particle state as a struct, update positions in a compute shader each frame, render as instanced geometry:
const Particle = d.struct({
position: d.vec2f,
velocity: d.vec2f,
life: d.f32,
});
const particles = root.createMutable(
d.arrayOf(Particle, 10000),
initialData,
);
const time = root.createUniform(d.f32);
// Compute: update state
const updateCompute = tgpu.computeFn({
workgroupSize: [256],
in: { gid: d.builtin.globalInvocationId },
})(({ gid }) => {
'use gpu';
const i = gid.x;
const p = particles.$[i];
particles.$[i].position = p.position.add(p.velocity.mul(time.$));
particles.$[i].life = p.life - 0.01;
});
// Render: draw each particle as instanced geometry
// Vertex shader reads from particles buffer via instance indexProcedural noise effects
Use @typegpu/noise for Perlin noise, random distributions, and procedural generation:
npm install @typegpu/noiseimport { perlin2d, randf } from '@typegpu/noise';
const noiseFragment = tgpu.fragmentFn({
in: { uv: d.vec2f },
out: d.vec4f,
})(({ uv }) => {
'use gpu';
const noise = perlin2d.sample(uv.mul(10));
return d.vec4f(noise, noise, noise, 1);
});For better performance on large textures, precompute gradients with a static or dynamic cache:
const cache = perlin3d.staticCache({ root, size: d.vec3u(64, 64, 1) });
const pipeline = root
.pipe(cache.inject())
.createRenderPipeline({ /* ... */ });Caching provides up to 10x performance improvement over on-demand gradient computation.
Available from @typegpu/noise:
- PRNG:
randf.sample(),randf.seed2()for per-thread seeding - Distributions:
exponential,normal,cauchy,bernoulli - Geometric:
inUnitCircle,onUnitSphere,inHemisphere, and more - Perlin noise:
perlin2d.sample(),perlin3d.sample()with optional caching
SDF rendering
Use @typegpu/sdf for procedural shape rendering and ray marching:
npm install @typegpu/sdfimport * as sdf from '@typegpu/sdf';
const sdfFragment = tgpu.fragmentFn({
in: { uv: d.vec2f },
out: d.vec4f,
})(({ uv }) => {
'use gpu';
const centered = uv.sub(0.5);
const dist = sdf.sdDisk(centered, 0.25);
if (dist < 0) {
return d.vec4f(0.2, 0.6, 1.0, 1);
}
return d.vec4f(0, 0, 0, 1);
});Shapes are positioned at origin. Apply inverse transforms to the point for translation, rotation, and scale.
Boids / flocking simulation
A classic GPU compute pattern: each boid reads neighbors from a storage buffer in the compute pass, updates its velocity, then renders as instanced triangles.
Fluid simulation
Grid-based velocity field stored in a storage buffer or texture. Compute passes handle advection, pressure solving, and diffusion. Render the velocity field as colors or displace geometry.
---
Three.js and React Three Fiber
For 3D scenes, react-native-wgpu supports Three.js (from r168+) and React Three Fiber:
1. Modify Metro config to resolve Three.js to the WebGPU build. 2. Patch @react-three/fiber/package.json to use the WebGPU entry point instead of the React Native bundle:
- "react-native": "native/dist/react-three-fiber-native.cjs.js",
+ "react-native": "dist/react-three-fiber.cjs.js",This enables skeletal animation, cloth simulation, particle effects, and complex 3D scene graphs using the Three.js ecosystem.
---
Performance Notes
- GPU compute runs thousands of threads in parallel with zero JS overhead.
- Frame presentation is manual (
context.present()). You control timing. - TypeGPU pipelines are created lazily on first execution, avoiding upfront compilation cost.
- Use
root.unwrap()to access raw WebGPU resources when needed for interop. - Timestamp queries (
withPerformanceCallback) provide nanosecond-precision GPU timing for profiling. - TypeGPU and raw WebGPU can be mixed freely. Adoption is non-contagious: use TypeGPU where type safety helps and raw WebGPU everywhere else.
Layout Animations
Animations for components entering, exiting, or changing position in the view hierarchy. Reanimated 4, New Architecture required.
For predefined animation lists, modifiers, and parameters, webfetch the linked documentation pages below.
---
Entering and Exiting Animations
Animate elements when they are added to or removed from the view hierarchy:
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
{visible && (
<Animated.View entering={FadeIn} exiting={FadeOut}>
<Text>Hello</Text>
</Animated.View>
)}Predefined animation families include Fade, Slide, Zoom, Bounce, Flip, Stretch, Roll, Rotate, LightSpeed, and Pinwheel. Each has directional variants (e.g., FadeInRight, FadeInLeft, FadeInUp, FadeInDown).
Chain modifiers on any predefined animation:
entering={FadeIn.duration(500).delay(200).springify().damping(15)}Time-based modifiers (.duration(), .easing()) are incompatible with spring-based modifiers (.springify(), .damping(), .mass(), .stiffness()).
Gotchas
- `nativeID` conflict (New Architecture): Reanimated uses
nativeIDinternally for entering animations. Overwriting it breaks the animation. Wrap animated children in a plainViewto work around this, especially withTouchableWithoutFeedback. - View flattening: Removing a non-animated parent triggers exiting animations in its children, but the parent will not wait for children to finish. Add
collapsable={false}to the parent to prevent this. - Spring-based animations: Not yet available on the web platform.
- Performance: Define animation builders outside of components or wrap with
useMemo.
---
Layout Transitions
Smooth animations when a component's position or size changes due to state updates:
import Animated, { LinearTransition } from 'react-native-reanimated';
<Animated.View layout={LinearTransition}>
{items.map((item) => (
<Item key={item.id} {...item} />
))}
</Animated.View>Predefined transitions: LinearTransition, SequencedTransition, FadingTransition, JumpingTransition, CurvedTransition, EntryExitTransition.
The generic Layout transition from older Reanimated versions is deprecated. Use LinearTransition.
Spring config modes: Use either physics-based (damping/stiffness) or duration-based (duration/dampingRatio), never both. If both are provided, duration-based overrides.
---
Keyframe Animations
For complex multi-step entering/exiting animations beyond what presets offer:
import { Keyframe } from 'react-native-reanimated';
const enteringAnimation = new Keyframe({
0: { opacity: 0, transform: [{ scale: 0.5 }, { rotate: '-45deg' }] },
50: {
opacity: 1,
transform: [{ scale: 1.2 }, { rotate: '0deg' }],
easing: Easing.out(Easing.quad),
},
100: { transform: [{ scale: 1 }, { rotate: '0deg' }] },
});
<Animated.View entering={enteringAnimation.duration(600)} />Rules
- Keyframe
0(orfrom) is required. Provide initial values for all properties you intend to animate. - Keyframe
100(orto) is optional. - Do not provide both
0andfrom, or both100andto. - Easing is assigned to the second keyframe in a pair. Never provide easing to keyframe
0. - Default easing between keyframes is
Easing.linear. - All properties in the transform array must appear in the same order across all keyframes.
---
List Layout Animations
Animate item layout changes in FlatList when items are added, removed, or reordered:
<Animated.FlatList
data={data}
renderItem={renderItem}
itemLayoutAnimation={LinearTransition}
/>Rules
- Only works with single-column
FlatList.numColumnscannot be greater than 1. - Items must have a
keyoridproperty (or provide a customkeyExtractor). - Set
itemLayoutAnimationtoundefinedto disable at runtime. - Use
.skipEnteringExitingAnimationsto prevent entering/exiting animations on initial mount and unmount of the FlatList.
---
LayoutAnimationConfig
Skip entering/exiting animations for a subtree:
import { LayoutAnimationConfig } from 'react-native-reanimated';
<LayoutAnimationConfig skipEntering skipExiting>
{children}
</LayoutAnimationConfig>Can be nested. For FlatLists, use the .skipEnteringExitingAnimations modifier on itemLayoutAnimation instead.
---
Shared Element Transitions
Status: Experimental. Not recommended for production.
Animates a view between two screens during navigation:
<Animated.Image
sharedTransitionTag="hero-image"
sharedTransitionStyle={SharedTransition.duration(550).springify()}
/>- Requires React Navigation native stack navigator. Tab navigator and
transparentModal(iOS) are not supported. - Tags must be unique per screen. Add the same tag to matching components on both screens.
- Default duration: 500ms. Animates width, height, position, transform, backgroundColor, opacity.
- iOS supports progress-based (swipe gesture) transitions. Android uses timing-based transitions only.
- Custom animation functions are not yet supported.
Scroll, Events, and Utilities
Patterns for scroll-driven animations, event-based reactions, frame callbacks, and measurement in Reanimated 4.
For API signatures and parameter details, webfetch the linked documentation pages below.
---
Scroll-Driven Animations
useAnimatedScrollHandler
Respond to scroll events with multiple handlers:
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event, context) => {
offset.value = event.contentOffset.y;
},
onBeginDrag: (event, context) => {
context.startY = event.contentOffset.y;
},
onEndDrag: (event, context) => {
if (event.contentOffset.y - context.startY > 100) {
offset.value = withSpring(200);
}
},
});
<Animated.ScrollView onScroll={scrollHandler}>
{children}
</Animated.ScrollView>Available handlers: onScroll, onBeginDrag, onEndDrag, onMomentumBegin, onMomentumEnd.
The context object is shared between all handlers for the same component, letting you pass state across events (e.g., save drag start position in onBeginDrag, read it in onEndDrag).
Passing a single function instead of an object is treated as onScroll.
Gotchas:
- Must use
Animated.ScrollView, not plainScrollView. - On Web, only
onScrollfires. Other events are iOS/Android only.
useScrollOffset
Simpler alternative when you only need the scroll position as a shared value:
const animatedRef = useAnimatedRef<Animated.ScrollView>();
const scrollOffset = useScrollOffset(animatedRef);
const headerStyle = useAnimatedStyle(() => ({
opacity: interpolate(scrollOffset.value, [0, 100], [1, 0]),
}));Automatically detects horizontal or vertical scroll. Works with ScrollView, FlatList, and FlashList. The ref can be changed at runtime.
scrollTo
Programmatic scrolling from the UI thread:
const animatedRef = useAnimatedRef<Animated.ScrollView>();
// In a worklet or useDerivedValue
scrollTo(animatedRef, 0, targetY, true); // (ref, x, y, animated)Can only be called from the UI thread. Wrap with scheduleOnUI() when calling from RN-thread event handlers.
---
Value Mapping Utilities
interpolate
Maps a numeric value from one range to another:
const opacity = interpolate(scrollOffset.value, [0, 100, 200], [1, 0.5, 0]);Input values must be in increasing order.
interpolateColor
Maps a numeric value to a color, producing smooth color transitions:
const color = interpolateColor(progress.value, [0, 1], ['#ff0000', '#0000ff'], 'RGB');Color spaces: 'RGB' (default), 'HSV', 'LAB' (Oklab, perceptually uniform).
---
Event Reactions
useAnimatedReaction
React to shared value changes with access to both current and previous values:
useAnimatedReaction(
() => Math.floor(scrollOffset.value / PAGE_HEIGHT),
(currentPage, previousPage) => {
if (previousPage !== null && currentPage !== previousPage) {
scheduleOnRN(onPageChanged, currentPage);
}
}
);The prepare function transforms/filters shared values before comparison. The react function fires when the prepared value changes.
Critical: Do not mutate the same shared value in react that you track in prepare. This causes an infinite loop.
Use prepare to reduce callback frequency (e.g., Math.floor() to react only on whole page changes instead of every pixel).
---
Frame Callbacks
useFrameCallback
Run logic on every frame (60Hz or 120Hz depending on the device):
const frameCallback = useFrameCallback((frameInfo) => {
progress.value += (frameInfo.timeSincePreviousFrame ?? 0) * speed;
});
// Pause/resume
frameCallback.setActive(false);
frameCallback.setActive(true);autostart(second parameter, defaulttrue) controls whether the callback begins immediately.- Always memoize the callback with
useCallbackto avoid recreation on every render. - Use time deltas (
timeSincePreviousFrame) for frame-rate-independent animations.
---
Measurement
measure
Synchronously get a view's dimensions and position on the UI thread:
const animatedRef = useAnimatedRef<Animated.View>();
const animatedStyle = useAnimatedStyle(() => {
if (!_WORKLET) return {}; // Guard: first evaluation runs on JS thread
const measurements = measure(animatedRef);
if (measurements === null) return {};
return {
transform: [{ translateY: -measurements.height }],
};
});Returns { x, y, width, height, pageX, pageY } or null if the component is unmounted or off-screen (e.g., recycled FlatList items).
Rules:
- Always check for
nullbefore using measurements. - In
useAnimatedStyle, guard withif (!_WORKLET) return {}because the first evaluation runs on the JS thread wheremeasureis unavailable. - Wrap with
scheduleOnUI()when calling from RN-thread event handlers. - Not available with Remote JS Debugger (use Chrome DevTools).
---
Device Sensors
`useAnimatedSensor` tracks device motion (accelerometer, gyroscope, rotation) for parallax and tilt animations. iOS requires location services enabled. Web requires HTTPS.
---
Keyboard (Deprecated)
`useAnimatedKeyboard` is deprecated in Reanimated 4. Use react-native-keyboard-controller for keyboard-aware animations.
SVG Animation Patterns
react-native-svg implements the SVG standard as a React component tree, giving you full control over every element in an SVG — including the ability to animate or dynamically change individual parts. Every SVG element becomes a native view.
---
Animating SVG Elements with Reanimated
Use Animated.createAnimatedComponent to make any SVG element animatable.
Do all value conversions directly inside the useAnimatedStyle or useAnimatedProps callback. Do not use SVGAdapter — handle conversions (string formatting, color processing, unit calculations) in place within the callback:
import Animated, {
useSharedValue,
useAnimatedProps,
withRepeat,
withTiming,
} from 'react-native-reanimated';
import { Circle, Svg } from 'react-native-svg';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
export default function PulsingCircle() {
const radius = useSharedValue(30);
radius.value = withRepeat(withTiming(50, { duration: 600 }), -1, true);
const animatedProps = useAnimatedProps(() => ({
r: radius.value,
}));
return (
<Svg width={120} height={120}>
<AnimatedCircle cx={60} cy={60} animatedProps={animatedProps} fill="tomato" />
</Svg>
);
}---
Animating SVG Path (e.g. Progress Arc)
import Animated, {
useSharedValue,
useAnimatedProps,
withTiming,
} from 'react-native-reanimated';
import { Path, Svg } from 'react-native-svg';
const AnimatedPath = Animated.createAnimatedComponent(Path);
export default function ProgressArc({ progress }: { progress: number }) {
const animatedProgress = useSharedValue(0);
animatedProgress.value = withTiming(progress, { duration: 800 });
const animatedProps = useAnimatedProps(() => {
const angle = animatedProgress.value * 2 * Math.PI;
const x = 60 + 50 * Math.cos(angle - Math.PI / 2);
const y = 60 + 50 * Math.sin(angle - Math.PI / 2);
const largeArc = animatedProgress.value > 0.5 ? 1 : 0;
return {
d: `M 60 10 A 50 50 0 ${largeArc} 1 ${x} ${y}`,
};
});
return (
<Svg width={120} height={120}>
<AnimatedPath animatedProps={animatedProps} stroke="tomato" strokeWidth={4} fill="none" />
</Svg>
);
}Audio Core Concepts
Core concepts and patterns for react-native-audio-api. Follows the Web Audio API specification for cross-platform consistency across iOS, Android, and web.
For playback patterns, see `playback.md`. For recording, see `recording.md`. For effects and analysis, see `effects-and-analysis.md`. For worklets, see `worklets.md`. For system integration, see `system-and-notifications.md`.
---
Decision Tree
Pick the approach based on what the feature needs.
Do you need simple file playback with no effects or real-time processing?
├── YES → Consider Expo Audio (simpler API, less control)
└── NO → Use react-native-audio-api
├── Playing pre-recorded audio files → AudioBufferSourceNode (see playback.md)
├── Generating tones or periodic signals → OscillatorNode (see playback.md)
├── Streaming HLS audio → StreamerNode (see playback.md, mobile only)
├── Playing sequential buffers → AudioBufferQueueSourceNode (see playback.md, mobile only)
├── Recording from microphone → AudioRecorder (see recording.md)
├── Applying effects to audio → GainNode, BiquadFilterNode, etc. (see effects-and-analysis.md)
├── Visualizing audio data → AnalyserNode (see effects-and-analysis.md)
├── Custom real-time audio processing → Worklets (see worklets.md)
└── System integration (notifications, interruptions) → AudioManager (see system-and-notifications.md)---
AudioContext Lifecycle
AudioContext is the central object that controls the audio-processing graph. It manages node creation, audio decoding, and the rendering lifecycle.
Singleton Pattern
Encapsulate AudioContext in a singleton class to keep audio logic outside of React components and maintain consistent state across the app.
import { AudioContext } from 'react-native-audio-api';
class AudioManager {
private static instance: AudioManager;
readonly context: AudioContext;
private constructor() {
this.context = new AudioContext();
}
static getInstance(): AudioManager {
if (!AudioManager.instance) {
AudioManager.instance = new AudioManager();
}
return AudioManager.instance;
}
}
export const audioManager = AudioManager.getInstance();Context States
running: The audio context is actively processing audio.suspended: Time progression is paused. Usesuspend()when audio is temporarily not needed.closed: The context is permanently shut down. Useclose()when the audio feature is done.
Mount/Unmount Pattern
Activate the context when the audio feature mounts, suspend on unmount:
useEffect(() => {
const ctx = audioManager.context;
if (ctx.state === 'suspended') {
ctx.resume();
}
return () => {
ctx.suspend();
};
}, []);Call close() only when the audio feature is permanently torn down. Wrapping every play/record call in activate/deactivate pairs adds perceptible latency.
---
Audio Graph
An audio graph is a network of nodes connected in a signal flow:
- Source nodes generate or provide audio data (
AudioBufferSourceNode,OscillatorNode,StreamerNode,RecorderAdapterNode) - Effect nodes transform audio data (
GainNode,BiquadFilterNode,DelayNode,ConvolverNode,StereoPannerNode,WaveShaperNode) - Analysis nodes extract data without modifying the signal (
AnalyserNode) - Destination node represents the final output (
audioContext.destination)
Nodes connect with .connect() and disconnect with .disconnect():
const source = audioContext.createBufferSource();
const gain = audioContext.createGain();
const analyser = audioContext.createAnalyser();
source.connect(gain);
gain.connect(analyser);
analyser.connect(audioContext.destination);Audio is rendered in blocks of 128 sample-frames (render quantum). The system audio callback drives the rendering thread.
---
Decoding Audio Data
Use decodeAudioData to load audio from files, URLs, ArrayBuffers, or bundled assets:
// From a URL or local file path
const buffer = await audioContext.decodeAudioData('https://example.com/audio.mp3');
// From an ArrayBuffer
const buffer = await audioContext.decodeAudioData(arrayBuffer);
// From a bundled asset (mobile only)
const buffer = await audioContext.decodeAudioData(require('./audio.mp3'));The audio is automatically resampled to match the context's sampleRate. Pass an optional sampleRate parameter to override.
For base64-encoded PCM data, use decodePCMInBase64:
const buffer = await audioContext.decodePCMInBase64(base64String, 44100, 2, true);For full format support details, webfetch the decoding docs.
---
Storing AudioBuffers in State
AudioBuffer objects are safe to store in React state, Zustand, Redux, or any state container. The buffer holds a reference to native memory. Copying that reference into state does not copy the audio data, so there is no performance concern.
const [buffer, setBuffer] = useState<AudioBuffer | null>(null);
async function load(url: string) {
const loaded = await audioContext.decodeAudioData(url);
setBuffer(loaded);
}---
Creating Buffers Programmatically
Use createBuffer for procedural audio (noise, tones, test signals):
const sampleRate = audioContext.sampleRate;
const bufferSize = sampleRate * 2; // 2 seconds
const buffer = audioContext.createBuffer(1, bufferSize, sampleRate);
const channelData = new Float32Array(bufferSize);
for (let i = 0; i < bufferSize; i++) {
channelData[i] = Math.random() * 2 - 1; // white noise
}
buffer.copyToChannel(channelData, 0, 0);---
References
Audio Effects and Analysis
Patterns for building audio effect chains and analyzing audio data in React Native using react-native-audio-api.
For core concepts, see `audio.md`. For playback sources, see `playback.md`.
---
Effect Nodes Overview
Effect nodes transform audio passing through them. Chain them between source and destination:
source.connect(effectA);
effectA.connect(effectB);
effectB.connect(audioContext.destination);All effect nodes are created from AudioContext (or BaseAudioContext) factory methods.
---
GainNode
Controls volume. The most commonly used effect node.
const gain = audioContext.createGain();
gain.gain.setValueAtTime(0.5, audioContext.currentTime); // 50% volume
source.connect(gain);
gain.connect(audioContext.destination);Fade In / Fade Out
// Fade in over 2 seconds
gain.gain.setValueAtTime(0, audioContext.currentTime);
gain.gain.linearRampToValueAtTime(1, audioContext.currentTime + 2);
// Fade out over 0.5 seconds
gain.gain.setValueAtTime(1, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.5);
gain.gain.setValueAtTime(0, audioContext.currentTime + 0.51);Gotcha: exponentialRampToValueAtTime cannot reach 0. Ramp to a tiny value (0.001), then set to 0.
ADSR Envelope
Use GainNode to shape how a sound's volume changes over time. This is essential for musical instruments and sound design.
function playNoteWithEnvelope(
audioContext: AudioContext,
buffer: AudioBuffer,
attack: number,
decay: number,
sustain: number,
release: number
) {
const source = audioContext.createBufferSource();
const envelope = audioContext.createGain();
const now = audioContext.currentTime;
source.buffer = buffer;
source.connect(envelope);
envelope.connect(audioContext.destination);
// Attack: silence → peak
envelope.gain.setValueAtTime(0.001, now);
envelope.gain.exponentialRampToValueAtTime(1.0, now + attack);
// Decay: peak → sustain level
envelope.gain.exponentialRampToValueAtTime(sustain, now + attack + decay);
source.start(now);
return {
release: () => {
const releaseStart = audioContext.currentTime;
// Release: current → silence
envelope.gain.setValueAtTime(envelope.gain.value, releaseStart);
envelope.gain.exponentialRampToValueAtTime(0.001, releaseStart + release);
envelope.gain.setValueAtTime(0, releaseStart + release + 0.01);
source.stop(releaseStart + release + 0.02);
},
};
}For GainNode API details, webfetch the GainNode docs.
---
BiquadFilterNode
Implements common audio filters: lowpass, highpass, bandpass, notch, allpass, peaking, lowshelf, highshelf.
const filter = audioContext.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 1000; // cutoff frequency in Hz
filter.Q.value = 1.0; // quality factor
source.connect(filter);
filter.connect(audioContext.destination);Common Filter Types
| Type | Effect |
|---|---|
lowpass | Passes frequencies below cutoff, attenuates above |
highpass | Passes frequencies above cutoff, attenuates below |
bandpass | Passes a range around the center frequency |
notch | Rejects a narrow band around the center frequency |
peaking | Boosts/cuts around the center frequency (parametric EQ) |
lowshelf | Boosts/cuts all frequencies below the shelf frequency |
highshelf | Boosts/cuts all frequencies above the shelf frequency |
For BiquadFilterNode API details, webfetch the BiquadFilterNode docs.
---
DelayNode
Delays the audio signal by a specified time.
const delay = audioContext.createDelay(5.0); // max delay time in seconds
delay.delayTime.value = 0.3; // 300ms delay
source.connect(delay);
delay.connect(audioContext.destination);Echo Effect
Create a feedback loop with a gain node to produce echo:
const delay = audioContext.createDelay(1.0);
const feedback = audioContext.createGain();
delay.delayTime.value = 0.4;
feedback.gain.value = 0.5; // each echo is 50% quieter
source.connect(delay);
delay.connect(feedback);
feedback.connect(delay); // feedback loop
delay.connect(audioContext.destination);
source.connect(audioContext.destination); // dry signalFor DelayNode API details, webfetch the DelayNode docs.
---
ConvolverNode
Applies convolution reverb using an impulse response buffer. Great for adding room acoustics or spatial effects.
const convolver = audioContext.createConvolver();
convolver.buffer = impulseResponseBuffer; // an AudioBuffer with the IR
source.connect(convolver);
convolver.connect(audioContext.destination);For ConvolverNode API details, webfetch the ConvolverNode docs.
---
StereoPannerNode
Positions audio in the stereo field.
const panner = audioContext.createStereoPanner();
panner.pan.value = -1; // full left (-1 to 1)
source.connect(panner);
panner.connect(audioContext.destination);For StereoPannerNode API details, webfetch the StereoPannerNode docs.
---
WaveShaperNode
Applies non-linear distortion using a shaping curve.
const shaper = audioContext.createWaveShaper();
shaper.curve = makeDistortionCurve(400);
shaper.oversample = '4x'; // '2x' | '4x' | 'none'
source.connect(shaper);
shaper.connect(audioContext.destination);For WaveShaperNode API details, webfetch the WaveShaperNode docs.
---
IIRFilterNode
Implements a general IIR (Infinite Impulse Response) filter when BiquadFilterNode types are insufficient.
const iir = audioContext.createIIRFilter(feedforward, feedback);For IIRFilterNode API details, webfetch the IIRFilterNode docs.
---
AnalyserNode
Extracts time-domain and frequency-domain data from audio without modifying the signal. Essential for audio visualizations.
const analyser = audioContext.createAnalyser();
analyser.fftSize = 2048; // power of 2, between 32 and 32768
analyser.smoothingTimeConstant = 0.8; // 0 (no smoothing) to 1 (heavy smoothing)
source.connect(analyser);
analyser.connect(audioContext.destination);Extracting Data
// Frequency data (for spectrum visualizers, EQ displays)
const freqData = new Float32Array(analyser.frequencyBinCount);
analyser.getFloatFrequencyData(freqData); // values in dB
const freqBytes = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(freqBytes); // values 0-255
// Time-domain data (for waveform displays, oscilloscopes)
const timeData = new Float32Array(analyser.fftSize);
analyser.getFloatTimeDomainData(timeData); // values -1 to 1
const timeBytes = new Uint8Array(analyser.fftSize);
analyser.getByteTimeDomainData(timeBytes); // values 0-255 (127 = silence)Properties
fftSize: Must be a power of 2 in [32, 32768]. Larger values give finer frequency resolution but more latency.frequencyBinCount: AlwaysfftSize / 2. Number of frequency data points.minDecibels/maxDecibels: Range for byte frequency data mapping. Frequencies belowminDecibelsmap to 0, abovemaxDecibelsto 255.smoothingTimeConstant: Averaging with previous frame. Higher = smoother transitions.
---
Audio Visualization with Reanimated
When passing audio data to Reanimated shared values for animation, mutate the existing typed array in place to avoid GC jank:
import { useSharedValue } from 'react-native-reanimated';
const FFT_SIZE = 256;
const amplitudes = useSharedValue(new Float32Array(FFT_SIZE / 2));
function updateVisualization() {
const data = new Float32Array(analyser.frequencyBinCount);
analyser.getFloatFrequencyData(data);
amplitudes.modify((prev) => {
for (let i = 0; i < prev.length; i++) {
prev[i] = data[i];
}
return prev;
});
requestAnimationFrame(updateVisualization);
}Why `.modify()`? Assigning amplitudes.value = new Float32Array(data) allocates and garbage-collects a new array at 60fps or higher, causing jank. .modify() mutates the existing allocation on the UI thread, skipping GC entirely.
Visualization with React State (Simple Approach)
For simpler visualizations where maximum performance is less critical:
const [freqs, setFreqs] = useState(new Uint8Array(FFT_SIZE / 2));
function draw() {
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
setFreqs(data);
requestAnimationFrame(draw);
}This triggers React re-renders on every frame. For high-performance visualizations, prefer the Reanimated shared value approach above.
---
Building Effect Chains
Combine multiple effects in series:
const gain = audioContext.createGain();
const filter = audioContext.createBiquadFilter();
const delay = audioContext.createDelay(1.0);
const analyser = audioContext.createAnalyser();
// Source → filter → gain → delay → analyser → destination
source.connect(filter);
filter.connect(gain);
gain.connect(delay);
delay.connect(analyser);
analyser.connect(audioContext.destination);Dry/Wet Mix
Route the original signal alongside the effected signal:
const dryGain = audioContext.createGain();
const wetGain = audioContext.createGain();
dryGain.gain.value = 0.7;
wetGain.gain.value = 0.3;
source.connect(dryGain);
source.connect(effect);
effect.connect(wetGain);
dryGain.connect(audioContext.destination);
wetGain.connect(audioContext.destination);---
References
Audio Playback
Patterns for playing audio in React Native using react-native-audio-api source nodes.
For core concepts and AudioContext lifecycle, see `audio.md`. For audio effects, see `effects-and-analysis.md`.
---
AudioBufferSourceNode
The primary node for playing pre-recorded audio. Load audio into an AudioBuffer, assign it to a source node, connect to the destination, and start.
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start(audioContext.currentTime);Single-use rule: An AudioBufferSourceNode can be started only once. To replay the same sound, create a new node. The node is very inexpensive to create; always reuse the AudioBuffer.
Start with Scheduling
The start method accepts precise timing parameters:
// Start immediately
source.start();
// Start at a specific time
source.start(audioContext.currentTime + 1.0);
// Start at a time, from an offset, for a duration
source.start(audioContext.currentTime, 2.0, 5.0); // starts 2s in, plays for 5sTime values use the same coordinate system as audioContext.currentTime.
Looping
source.loop = true;
source.loopStart = 0.5; // seconds
source.loopEnd = 3.0; // seconds
source.start();Use the onLoopEnded event to know when the buffer loops:
source.onLoopEnded = () => {
console.log('Loop restarted');
};Playback Rate and Pitch
The playbackRate AudioParam changes the speed and pitch simultaneously:
source.playbackRate.value = 2.0; // double speed, one octave higherFor pitch correction (speed change without pitch change), create the source with pitchCorrection: true:
const source = audioContext.createBufferSource({ pitchCorrection: true });Pitch correction introduces processing latency. When scheduling precise playback times, start samples slightly ahead of the intended time. Use source.getLatency() to query the latency value. When pitchCorrection is enabled, playbackRate is clamped to [0, 3].
---
OscillatorNode
Generates periodic wave signals (sine, square, sawtooth, triangle). Useful for synthesizers, test tones, and sound design.
const osc = audioContext.createOscillator();
osc.type = 'sine'; // 'sine' | 'square' | 'sawtooth' | 'triangle'
osc.frequency.value = 440; // A4
osc.connect(audioContext.destination);
osc.start(audioContext.currentTime);
osc.stop(audioContext.currentTime + 2);Like AudioBufferSourceNode, oscillators are single-use. Create a new one for each tone.
Frequency and Detune
Both are AudioParam objects supporting scheduling:
// Slide from A4 to A5 over 1 second
osc.frequency.setValueAtTime(440, audioContext.currentTime);
osc.frequency.linearRampToValueAtTime(880, audioContext.currentTime + 1);
// Detune by one semitone (100 cents)
osc.detune.value = 100;Custom Waveforms
Use PeriodicWave for custom timbres:
const real = new Float32Array([0, 0.5, 0.3, 0.1]);
const imag = new Float32Array([0, 0, 0, 0]);
const wave = audioContext.createPeriodicWave(real, imag);
osc.setPeriodicWave(wave);For OscillatorNode API details, webfetch the OscillatorNode docs.
---
StreamerNode (Mobile Only)
Decodes and plays HTTP Live Streaming (HLS) audio data:
const streamer = audioContext.createStreamer();
streamer.initialize('https://example.com/stream.m3u8');
streamer.connect(audioContext.destination);
streamer.start(audioContext.currentTime);For StreamerNode API details, webfetch the StreamerNode docs.
---
AudioBufferQueueSourceNode (Mobile Only)
A specialized source for playing sequential buffers. Useful for streaming audio chunks or playing a playlist of buffers without gaps.
const queue = audioContext.createBufferQueueSource();
const id1 = queue.enqueueBuffer(buffer1);
const id2 = queue.enqueueBuffer(buffer2);
queue.connect(audioContext.destination);
queue.start(audioContext.currentTime);Queue Management
queue.dequeueBuffer(id1); // remove a specific buffer
queue.clearBuffers(); // remove all queued buffersPause and Resume
Unlike AudioBufferSourceNode, AudioBufferQueueSourceNode supports true pause/resume:
queue.pause(); // halts playback, keeps position
queue.start(); // resumes from where it pausedBuffer End Events
queue.onBufferEnded = (event) => {
console.log(`Buffer ${event.bufferId} ended`);
if (event.isLastBufferInQueue) {
console.log('Queue exhausted');
}
};For AudioBufferQueueSourceNode API details, webfetch the AudioBufferQueueSourceNode docs.
---
AudioParam Scheduling
AudioParam controls time-varying properties on audio nodes (gain, frequency, delay time, etc.). Use scheduling methods for smooth, click-free transitions.
Available Methods
| Method | Use case |
|---|---|
setValueAtTime(value, time) | Instant change at a specific time |
linearRampToValueAtTime(value, endTime) | Linear fade between previous event and target |
exponentialRampToValueAtTime(value, endTime) | Exponential fade (perceptually even for volume) |
setTargetAtTime(target, startTime, timeConstant) | Asymptotic approach (good for decay/release) |
setValueCurveAtTime(values, startTime, duration) | Follow an arbitrary curve |
cancelScheduledValues(cancelTime) | Cancel all scheduled changes after a time |
cancelAndHoldAtTime(cancelTime) | Cancel and freeze at the current value |
Volume Fade Example
const gain = audioContext.createGain();
// Fade in over 2 seconds
gain.gain.setValueAtTime(0, audioContext.currentTime);
gain.gain.linearRampToValueAtTime(1, audioContext.currentTime + 2);Avoiding Clicks
Direct gain changes (e.g., gain.gain.value = 0) cause audible clicks. Always ramp:
// Bad: causes click
gain.gain.value = 0;
// Good: smooth fade
gain.gain.setValueAtTime(gain.gain.value, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.05);Gotcha: exponentialRampToValueAtTime cannot ramp to 0 (exponential never reaches zero). Use a very small value like 0.001, then setValueAtTime(0, ...) immediately after.
Gotcha: Avoid calling setValueAtTime more than ~31 times for continuous changes. Use ramp methods or setValueCurveAtTime instead for better performance.
For AudioParam API details, webfetch the AudioParam docs.
---
Noise Generation
Generate noise by filling an AudioBuffer with computed samples and looping it.
White Noise
function createWhiteNoise(audioContext: AudioContext): AudioBuffer {
const bufferSize = audioContext.sampleRate * 2;
const output = new Float32Array(bufferSize);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
const buffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate);
buffer.copyToChannel(output, 0, 0);
return buffer;
}
// Play it looped
const source = audioContext.createBufferSource();
source.buffer = createWhiteNoise(audioContext);
source.loop = true;
source.connect(audioContext.destination);
source.start();Pink Noise
Uses the Paul Kellet refined method for a -3dB/octave roll-off:
function createPinkNoise(audioContext: AudioContext): AudioBuffer {
const bufferSize = 2 * audioContext.sampleRate;
const output = new Float32Array(bufferSize);
let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;
for (let i = 0; i < bufferSize; i++) {
const white = Math.random() * 2 - 1;
b0 = 0.99886 * b0 + white * 0.0555179;
b1 = 0.99332 * b1 + white * 0.0750759;
b2 = 0.969 * b2 + white * 0.153852;
b3 = 0.8665 * b3 + white * 0.3104856;
b4 = 0.55 * b4 + white * 0.5329522;
b5 = -0.7616 * b5 - white * 0.016898;
output[i] = 0.11 * (b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362);
b6 = white * 0.115926;
}
const buffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate);
buffer.copyToChannel(output, 0, 0);
return buffer;
}Brownian Noise
-12dB/octave roll-off, sounds like a waterfall:
function createBrownianNoise(audioContext: AudioContext): AudioBuffer {
const bufferSize = 2 * audioContext.sampleRate;
const output = new Float32Array(bufferSize);
let lastOut = 0;
for (let i = 0; i < bufferSize; i++) {
const white = Math.random() * 2 - 1;
output[i] = (lastOut + 0.02 * white) / 1.02;
lastOut = output[i];
output[i] *= 3.5;
}
const buffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate);
buffer.copyToChannel(output, 0, 0);
return buffer;
}---
References
Audio Recording
Patterns for recording audio in React Native using AudioRecorder from react-native-audio-api.
For core concepts, see `audio.md`. For connecting recordings to the audio graph, see `effects-and-analysis.md`.
---
AudioRecorder Overview
AudioRecorder captures audio from the system microphone. It supports three modes of operation:
1. File recording: Writes audio data directly to the filesystem 2. Data callback: Emits raw audio buffers for streaming or custom processing 3. Graph processing: Connects the recorder to an AudioContext for real-time processing through the audio graph
All three modes can be active simultaneously.
Singleton Pattern
Create one AudioRecorder instance and reuse it. Switching between recorder instances has noticeable impact on device performance, memory, and battery.
import { AudioRecorder } from 'react-native-audio-api';
export const recorder = new AudioRecorder();---
Permissions
Recording requires microphone permissions. Configure them during app setup and request at runtime before starting.
Expo Setup
{
"plugins": [
[
"react-native-audio-api",
{
"iosMicrophonePermission": "This app needs microphone access to record audio.",
"androidPermissions": ["android.permission.RECORD_AUDIO"]
}
]
]
}Runtime Permission Request
import { AudioManager } from 'react-native-audio-api';
const status = await AudioManager.requestRecordingPermissions();
if (status !== 'Granted') {
console.warn('Microphone permission denied');
return;
}Use AudioManager.checkRecordingPermissions() to check without prompting.
---
File Recording
The simplest mode. Audio is encoded and written directly to disk.
import { AudioRecorder, AudioManager } from 'react-native-audio-api';
AudioManager.setAudioSessionOptions({
iosCategory: 'record',
iosMode: 'default',
iosOptions: [],
});
const recorder = new AudioRecorder();
recorder.enableFileOutput(); // default: M4A, high quality, cache directory
// Start recording
const permissions = await AudioManager.requestRecordingPermissions();
if (permissions !== 'Granted') return;
await AudioManager.setAudioSessionActivity(true);
const startResult = recorder.start();
if (startResult.status === 'error') {
console.warn(startResult.message);
return;
}
console.log('Recording to:', startResult.path);
// Stop recording
const stopResult = recorder.stop();
if (stopResult.status === 'success') {
console.log('File:', stopResult.path, 'Duration:', stopResult.duration);
}
AudioManager.setAudioSessionActivity(false);File Output Configuration
import { FileFormat, FilePreset, FileDirectory } from 'react-native-audio-api';
recorder.enableFileOutput({
format: FileFormat.M4A, // M4A | Wav | Caf | Flac
preset: FilePreset.High, // Lossless | High | Medium | Low
directory: FileDirectory.Document, // Document | Cache (default)
subDirectory: 'recordings',
fileNamePrefix: 'voice_note',
channelCount: 1,
});File Format Guide
| Format | Best for | Notes |
|---|---|---|
M4A | General recording, voice notes | Default. Good compression, wide compatibility |
Wav | Lossless capture | Large files, use with Lossless preset |
Caf | iOS lossless capture | Apple-specific container |
Flac | High quality with compression | Lossless compression, smaller than WAV |
Preset Guide
| Preset | Use case |
|---|---|
Lossless | Maximum quality, large files. Only with WAV or CAF |
High | Music, high-quality voice. Near-lossless perception |
Medium | Voice notes, podcasts. Good quality/size balance |
Low | Quick notes, diagnostics. Small files, speech-only |
Custom Preset
recorder.enableFileOutput({
format: FileFormat.M4A,
preset: {
bitRate: 128000,
sampleRate: 44100,
bitDepth: 16,
iosQuality: IOSAudioQuality.High,
flacCompressionLevel: FlacCompressionLevel.L5,
},
});File Name Override
const result = recorder.start({ fileNameOverride: `session_${sessionId}` });Disable File Output
recorder.disableFileOutput(); // finalizes current file if recording is active---
Data Callback
Delivers raw audio buffers periodically. Useful for streaming, speech-to-text, or custom processing on the JS thread.
const sampleRate = 16000;
recorder.onAudioReady(
{
sampleRate,
bufferLength: sampleRate * 0.1, // 100ms chunks
channelCount: 1,
},
({ buffer, numFrames, when }) => {
// buffer is an AudioBuffer with PCM data
// numFrames: number of audio frames in this chunk
// when: timestamp relative to recording start
}
);
// Clean up when done
recorder.clearOnAudioReady();The sampleRate, bufferLength, and channelCount are preferred values. Actual values may differ depending on hardware capabilities.
---
Graph Processing
Connects the recorder to the audio graph through a RecorderAdapterNode for real-time processing with effects, analysis, or worklets.
import { AudioRecorder, AudioContext, AudioManager } from 'react-native-audio-api';
AudioManager.setAudioSessionOptions({
iosCategory: 'playAndRecord',
iosMode: 'default',
iosOptions: [],
});
const recorder = new AudioRecorder();
const audioContext = new AudioContext();
const adapter = audioContext.createRecorderAdapter();
const gain = audioContext.createGain();
// Build the graph: recorder → adapter → gain → destination
adapter.connect(gain);
gain.connect(audioContext.destination);
recorder.connect(adapter);
await AudioManager.setAudioSessionActivity(true);
await audioContext.resume();
recorder.start();Use playAndRecord session category when both recording and playback are needed simultaneously.
Disconnect
recorder.disconnect(); // disconnects from the audio graph---
Pause and Resume
recorder.pause(); // pauses without finalizing the file
recorder.resume(); // resumes from where it pausedState Queries
recorder.isRecording(); // true if actively recording
recorder.isPaused(); // true if paused
recorder.getCurrentDuration(); // current recording duration (file output only)---
Error Handling
recorder.onError((error) => {
console.error('Recording error:', error.message);
});
// Clean up
recorder.clearOnError();---
Background Recording
To record while the app is in the background, configure platform permissions:
Expo
{
"plugins": [
[
"react-native-audio-api",
{
"iosBackgroundMode": true,
"iosMicrophonePermission": "Microphone access for recording.",
"androidPermissions": [
"android.permission.RECORD_AUDIO",
"android.permission.FOREGROUND_SERVICE",
"android.permission.FOREGROUND_SERVICE_MICROPHONE"
],
"androidForegroundService": true,
"androidFSTypes": ["microphone"]
}
]
]
}For bare React Native setup details, webfetch the AudioRecorder docs.
---
References
System Integration, Notifications, and Testing
Patterns for audio session management, system events, media notifications, permissions, and testing in react-native-audio-api.
For core concepts, see `audio.md`. For recording-specific setup, see `recording.md`.
---
AudioManager
AudioManager is the bridge between your app and the system audio layer. It configures iOS audio sessions, manages permissions, and provides system event listeners.
Import it as a static class:
import { AudioManager } from 'react-native-audio-api';---
iOS Audio Session Configuration
Configure the audio session before creating an AudioContext or starting recording. Session options control how your app interacts with other audio sources on the device.
AudioManager.setAudioSessionOptions({
iosCategory: 'playback',
iosMode: 'default',
iosOptions: ['defaultToSpeaker', 'allowBluetoothA2DP'],
});Session Categories
| Category | Use case |
|---|---|
playback | Audio/music playback. Silences other apps. |
record | Recording only. No playback output. |
playAndRecord | Simultaneous recording and playback. Use for voice chat, live monitoring, or graph processing with recorder. |
ambient | Non-essential audio (game sounds). Mixes with other apps, respects silent switch. |
soloAmbient | Like ambient but silences other apps. Default system category. |
multiRoute | Routes audio to multiple outputs simultaneously. |
Session Modes
| Mode | Use case |
|---|---|
default | Standard mode for most use cases |
voiceChat | Optimized for voice communication |
videoChat | Optimized for video calls |
gameChat | Optimized for game voice chat |
measurement | Audio measurement and analysis |
moviePlayback | Movie/video playback |
spokenAudio | Podcasts, audiobooks |
voicePrompt | Short voice prompts |
videoRecording | Video recording |
Session Options
| Option | Effect |
|---|---|
mixWithOthers | Mix audio with other apps instead of silencing them |
duckOthers | Lower other apps' volume while yours plays |
defaultToSpeaker | Route playback to speaker instead of earpiece |
allowBluetoothA2DP | Enable high-quality Bluetooth audio output |
allowBluetoothHFP | Enable Bluetooth hands-free profile (for calls) |
allowAirPlay | Enable AirPlay streaming |
interruptSpokenAudioAndMixWithOthers | Interrupt spoken audio from other apps, then mix |
overrideMutedMicrophoneInterruption | Continue recording when another app mutes the mic |
Session Activation
const success = await AudioManager.setAudioSessionActivity(true);
// Deactivate when done
AudioManager.setAudioSessionActivity(false);Disabling Internal Session Management
If you use another audio library alongside react-native-audio-api, disable internal session management and handle it yourself:
AudioManager.disableSessionManagement(); // call before creating AudioContextCalling setAudioSessionOptions or setAudioSessionActivity later re-enables internal management.
---
System Events
Audio Interruptions
Other apps or system events (phone calls, alarms) can interrupt your audio session:
AudioManager.observeAudioInterruptions(true);
const sub = AudioManager.addSystemEventListener('interruption', (event) => {
if (event.type === 'began') {
// Another app took audio focus. Pause your playback.
} else if (event.type === 'ended' && event.shouldResume) {
// Interruption ended and you can resume.
audioContext.resume();
}
});
// Cleanup
sub.remove();On Android, pass an AudioFocusType to observeAudioInterruptions to set the native audio focus:
AudioManager.observeAudioInterruptions('gain'); // 'gain' | 'gainTransient' | 'gainTransientExclusive' | 'gainTransientMayDuck'Volume Changes
AudioManager.observeVolumeChanges(true);
const sub = AudioManager.addSystemEventListener('volumeChange', (event) => {
console.log('New volume:', event.value);
});Route Changes
Detect headphone connects/disconnects and other routing changes:
const sub = AudioManager.addSystemEventListener('routeChange', (event) => {
console.log('Route changed:', event.reason);
// Reasons: 'NewDeviceAvailable', 'OldDeviceUnavailable', 'CategoryChange', etc.
});Audio Ducking
const sub = AudioManager.addSystemEventListener('duck', () => {
// System is asking your app to lower volume
});---
Active Session Reclaiming (iOS, Experimental)
In some cases, the system never sends an interruption ended event. Enable active reclaiming to automatically reactivate the session when other audio stops:
AudioManager.activelyReclaimSession(true);---
Device Information
const sampleRate = AudioManager.getDevicePreferredSampleRate();
const devices = await AudioManager.getDevicesInfo();
// { availableInputs, availableOutputs, currentInputs (iOS), currentOutputs (iOS) }---
PlaybackNotificationManager
Manages system-level media notifications with playback controls (play, pause, next, previous, seek).
import { PlaybackNotificationManager } from 'react-native-audio-api';
// Show notification with metadata
await PlaybackNotificationManager.show({
title: 'Song Title',
artist: 'Artist Name',
album: 'Album',
duration: 240,
state: 'playing',
});
// Listen for control actions
const playSub = PlaybackNotificationManager.addEventListener(
'playbackNotificationPlay',
() => {
audioContext.resume();
PlaybackNotificationManager.show({ state: 'playing' });
}
);
const pauseSub = PlaybackNotificationManager.addEventListener(
'playbackNotificationPause',
() => {
audioContext.suspend();
PlaybackNotificationManager.show({ state: 'paused' });
}
);
const seekSub = PlaybackNotificationManager.addEventListener(
'playbackNotificationSeekTo',
(event) => {
// event.value is the seek position in seconds
PlaybackNotificationManager.show({ elapsedTime: event.value });
}
);
// Update progress
PlaybackNotificationManager.show({ elapsedTime: 60 });
// Cleanup
playSub.remove();
pauseSub.remove();
seekSub.remove();
await PlaybackNotificationManager.hide();Platform Differences
iOS: Notification controls only appear when an AudioContext is running. show()/hide() only update metadata. To fully hide controls, suspend or close the AudioContext. Elapsed time does not auto-update; set it manually on each state change.
Android: show()/hide() directly control notification visibility. Works independently of AudioContext state.
Enable/Disable Controls
await PlaybackNotificationManager.enableControl('nextTrack', true);
await PlaybackNotificationManager.enableControl('seekTo', false);Available controls: play, pause, stop, nextTrack, previousTrack, skipForward, skipBackward, seekTo.
For PlaybackNotificationManager API details, webfetch the PlaybackNotificationManager docs.
---
RecordingNotificationManager (Android Only)
Shows a system notification with pause/resume controls for recording.
import { RecordingNotificationManager } from 'react-native-audio-api';
RecordingNotificationManager.show({
title: 'Recording',
contentText: 'Tap to pause',
paused: false,
smallIconResourceName: 'ic_mic',
pauseIconResourceName: 'ic_pause',
resumeIconResourceName: 'ic_play',
color: 0xff6200,
});
const pauseSub = RecordingNotificationManager.addEventListener(
'recordingNotificationPause',
() => {
recorder.pause();
RecordingNotificationManager.show({ paused: true, contentText: 'Paused' });
}
);
const resumeSub = RecordingNotificationManager.addEventListener(
'recordingNotificationResume',
() => {
recorder.resume();
RecordingNotificationManager.show({ paused: false, contentText: 'Recording...' });
}
);
// Cleanup
pauseSub.remove();
resumeSub.remove();
RecordingNotificationManager.hide();Resource names reference files in res/drawable without the extension (e.g., photo.png becomes photo).
For RecordingNotificationManager API details, webfetch the RecordingNotificationManager docs.
---
Permissions
Recording Permissions
// Request (shows system dialog)
const status = await AudioManager.requestRecordingPermissions();
// 'Granted' | 'Denied' | 'Undetermined'
// Check without prompting
const status = await AudioManager.checkRecordingPermissions();Throws an error if NSMicrophoneUsageDescription is missing from Info.plist (iOS).
Notification Permissions
const status = await AudioManager.requestNotificationPermissions();---
Testing with Mocks
react-native-audio-api provides a comprehensive mock implementation for unit testing without audio hardware.
Setup
// Option 1: Direct import
import { AudioContext, AudioRecorder } from 'react-native-audio-api/mock';
// Option 2: Module mock
jest.mock('react-native-audio-api', () =>
require('react-native-audio-api/mock')
);Testing Audio Graphs
import { AudioContext } from 'react-native-audio-api/mock';
it('should build an audio effect chain', () => {
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const filter = ctx.createBiquadFilter();
const gain = ctx.createGain();
filter.type = 'lowpass';
filter.frequency.value = 2000;
gain.gain.value = 0.8;
osc.connect(filter);
filter.connect(gain);
gain.connect(ctx.destination);
expect(filter.type).toBe('lowpass');
expect(gain.gain.value).toBe(0.8);
});Testing Recording
import { AudioRecorder, FileFormat } from 'react-native-audio-api/mock';
it('should record and stop', () => {
const recorder = new AudioRecorder();
recorder.enableFileOutput({ format: FileFormat.M4A });
const start = recorder.start();
expect(start.status).toBe('success');
expect(recorder.isRecording()).toBe(true);
const stop = recorder.stop();
expect(stop.status).toBe('success');
expect(recorder.isRecording()).toBe(false);
});Testing Context Lifecycle
it('should manage context state', async () => {
const ctx = new AudioContext();
expect(ctx.state).toBe('running');
await ctx.suspend();
expect(ctx.state).toBe('suspended');
await ctx.resume();
expect(ctx.state).toBe('running');
await ctx.close();
expect(ctx.state).toBe('closed');
});Testing Offline Rendering
import { OfflineAudioContext } from 'react-native-audio-api/mock';
it('should render offline', async () => {
const offCtx = new OfflineAudioContext({
numberOfChannels: 2,
length: 44100,
sampleRate: 44100,
});
const osc = offCtx.createOscillator();
osc.connect(offCtx.destination);
const buffer = await offCtx.startRendering();
expect(buffer.numberOfChannels).toBe(2);
expect(buffer.length).toBe(44100);
});For the full mock API, webfetch the testing docs.
---
References
When to Use React Native SVG
Use react-native-svg when you need interactive or animated control over individual SVG elements. For static SVGs, there are better options. For animated vector graphics where you don't need per-element control, consider Lottie or Rive.
Decision Guide
Do you need to animate or interactively control individual parts of the SVG?
│
├── YES → Use react-native-svg + Reanimated
│
└── NO
│
├── Is it an icon from a standard icon set?
│ └── YES → Use icon fonts (@expo/vector-icons, Font Awesome)
│
├── Is it a static SVG image?
│ └── YES → Use expo-image (preferred) or react-native-vector-image
│
├── Do you need animated vector graphics without per-element control?
│ └── YES → Use Lottie or Rive
│
└── Do you need complex SVGs with filters?
├── Need native filter support → react-native-skia
└── Can tolerate WebView overhead → WebView (browsers are among the best SVG renderers)Why prefer alternatives for static SVGs
react-native-svg creates a full React component tree and a matching native view hierarchy for every SVG element. None of these are memoized by default. Each native drawing operation redraws everything from scratch with no caching. Components like SvgXml and LocalSvg parse their input into the same React component tree under the hood, so they carry the same overhead.
For a static SVG from your design team, this means unnecessary reconciliation cost in React and unnecessary native views that only exist to pass props to the drawing layer.
Alternative Comparison
| Tool | Best for | Key trade-off |
|---|---|---|
expo-image | Static SVG images (delegates to Glide/SDWebImage natively) | Loads async, which can cause blinking on first render. Use preloading to minimize this. No filter support. |
Icon fonts (@expo/vector-icons, Font Awesome) | Icons | Limited to available glyphs in the font. Very performant. |
react-native-vector-image | Static SVGs as native assets (Vector Drawables on Android, PDF on iOS) | Build-time asset generation step required |
| Lottie / Rive | Animated vector graphics | Requires converting assets from SVG to Lottie/Rive format. Better animation performance than SVG. |
react-native-skia | Complex SVGs with filters, Reanimated integration | Each Canvas object is heavy. Rendering many small SVGs in separate Canvases degrades performance. Adds to package size. |
| WebView | SVGs with features no other renderer supports | Heavier than native options, but browsers cover the most of the SVG standard |
---