In Reanimated 4, how does useAnimatedScrollHandler drive a collapsing podcast header, and which scroll events can it handle?
answer
- worklets per scroll event
- onScroll, onBeginDrag, onEndDrag, onMomentumBegin, onMomentumEnd
- always passed to onScroll
- Animated.ScrollView, not ScrollView
- a context object shared across events
basics
~20 suseAnimatedScrollHandler returns a handler whose worklets run on the UI runtime for onScroll, onBeginDrag, onEndDrag, onMomentumBegin and onMomentumEnd. Pass it to onScroll of Reanimated's Animated.ScrollView or Animated.FlatList, write the offset to a shared value, and interpolate the header.
solid answer
~40 s`useAnimatedScrollHandler` takes either one worklet, treated as the `onScroll` handler, or an object with `onScroll`, `onBeginDrag`, `onEndDrag`, `onMomentumBegin` and `onMomentumEnd`. Each worklet receives the scroll event (`event.contentOffset.y` and the usual `ScrollView` fields) and a `context` object that persists between events and is shared by all the handlers. The returned handler always goes to the `onScroll` prop, even when it handles drag or momentum events, and only works on Animated-wrapped scrollables such as Reanimated's `Animated.ScrollView` or `Animated.FlatList`. For the header, write `scrollY.value = event.contentOffset.y` and derive `translateY` and `opacity` in `useAnimatedStyle` with `interpolate(..., Extrapolation.CLAMP)`. If you only need the offset, `useScrollOffset(animatedRef)` returns it as a shared value; it was called `useScrollViewOffset` before Reanimated 4.
code
tsx · 56 linesimport { StyleSheet, Text } from 'react-native';
import Animated, {
Extrapolation,
interpolate,
useAnimatedScrollHandler,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';
const HEADER_HEIGHT = 240;
const COLLAPSED_HEIGHT = 88;
const RANGE = HEADER_HEIGHT - COLLAPSED_HEIGHT;
type Episode = { id: string; title: string };
export function ShowScreen({ episodes }: { episodes: Episode[] }) {
const scrollY = useSharedValue(0);
const onScroll = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
const headerStyle = useAnimatedStyle(() => ({
transform: [
{ translateY: interpolate(scrollY.value, [0, RANGE], [0, -RANGE], Extrapolation.CLAMP) },
],
}));
const artworkStyle = useAnimatedStyle(() => ({
opacity: interpolate(scrollY.value, [0, RANGE / 2], [1, 0], Extrapolation.CLAMP),
}));
return (
<>
<Animated.FlatList
data={episodes}
keyExtractor={(e) => e.id}
renderItem={({ item }) => <Text style={styles.row}>{item.title}</Text>}
onScroll={onScroll}
contentContainerStyle={{ paddingTop: HEADER_HEIGHT }}
/>
<Animated.View style={[styles.header, headerStyle]}>
<Animated.View style={[styles.artwork, artworkStyle]} />
<Text style={styles.title}>Show title</Text>
</Animated.View>
</>
);
}
const styles = StyleSheet.create({
header: { position: 'absolute', top: 0, left: 0, right: 0, height: HEADER_HEIGHT, backgroundColor: 'white' },
artwork: { width: 120, height: 120, alignSelf: 'center', marginTop: 24, backgroundColor: 'gray' },
title: { position: 'absolute', bottom: 16, alignSelf: 'center', fontSize: 20 },
row: { padding: 16 },
});go deeper
Recall that useAnimatedScrollHandler returns a handler for onScroll, that it needs Reanimated's Animated.ScrollView or Animated.FlatList, and that it writes the offset to a shared value.
Explain the five event keys, the shared context object, clamped interpolation for the header, and when useScrollOffset is the simpler tool.
Add snapping on drag or momentum end, cross to React only at thresholds, and know the New Architecture flicker fixes Reanimated documents.
Decide where scroll-linked effects live in the codebase, Reanimated or core Animated, so screens share one pattern and one set of performance flags.
## What the hook gives you `useAnimatedScrollHandler` is Reanimated's way to react to scrolling **on the UI runtime**. It returns an event handler object that you attach to a scrollable. Each time the native scroll view dispatches an event, the matching worklet runs on the UI thread, without a trip to the JS thread and without a React render. It accepts two shapes: - **A single worklet** `(event, context) => { ... }`, treated as the `onScroll` handler. - **An object of worklets** keyed by event name: `onScroll`, `onBeginDrag`, `onEndDrag`, `onMomentumBegin`, `onMomentumEnd`. Each worklet receives: 1. `event`, the scroll payload, with the same structure as React Native's `ScrollView` events: `contentOffset`, `contentSize`, `layoutMeasurement` and so on. 2. `context`, a plain object that **persists between events**. When you pass several handlers, they share the same `context`, so `onBeginDrag` can record a starting offset that `onScroll` later reads. ## Wiring it up correctly Two rules trip people up: - **Always pass it to `onScroll`.** Even a handler that only defines `onBeginDrag` and `onMomentumEnd` is passed as `onScroll={handler}`; Reanimated registers the other events itself. - **Use an Animated-wrapped scrollable.** The handler is triggered only on components wrapped with Reanimated's `Animated`, such as `Animated.ScrollView` or `Animated.FlatList` from `react-native-reanimated`, not on a plain `ScrollView` from `react-native`. The same handler object may be passed to several scrollables; its worklets then run for events from any of them. ## Building the collapsing header For a podcast's show page with a large artwork header over the episode list: 1. Keep the offset in a shared value: `scrollY.value = event.contentOffset.y`. 2. Derive the header's `translateY` with `interpolate(scrollY.value, [0, RANGE], [0, -RANGE], Extrapolation.CLAMP)`. 3. Fade the artwork with a shorter input range for `opacity`. 4. Give the list `contentContainerStyle={{ paddingTop: HEADER_HEIGHT }}` so the first episode starts below the header. `Extrapolation.CLAMP` matters: without it, iOS's bounce at the top produces negative offsets and the header would slide *down* past its resting position. ## Using the other events | Event key | Fires when | Typical use in a header | |---|---|---| | `onBeginDrag` | The finger starts dragging | Store the starting offset in `context` | | `onScroll` | Each scroll frame | Update the shared value | | `onEndDrag` | The finger lifts | Decide where a half-collapsed header should settle | | `onMomentumBegin` | Glide after release starts | Rarely needed for headers | | `onMomentumEnd` | Glide stops | Snap a partially hidden header open or closed | On the web only `onScroll` is supported, because there are no native drag and momentum events. ## When you only need the offset `useScrollOffset(animatedRef)` returns a shared value holding the current offset of the scrollable that `animatedRef` (from `useAnimatedRef`) is attached to. It detects horizontal versus vertical automatically and works with `ScrollView`, `FlatList` and `FlashList`. Reanimated 4 renamed it from `useScrollViewOffset`, which remains only as a deprecated alias. Reach for `useAnimatedScrollHandler` when you need per-event logic or drag and momentum events. ## Crossing back to React The handler is a worklet, so it cannot call `setState` directly. For a discrete change, such as revealing a compact title bar, compare against a threshold in the worklet and call `scheduleOnRN` only when the threshold is crossed. ## Common mistakes - **Passing the handler to a plain `ScrollView`.** Nothing fires; use Reanimated's `Animated.ScrollView` or `Animated.FlatList`. - **Splitting the handler across props.** Drag and momentum worklets still go in the one object passed to `onScroll`. - **Interpolating without clamping.** Bounce offsets push the header past its resting or collapsed position. - **Calling React from the worklet.** `setState` inside the handler throws; cross with `scheduleOnRN` at a threshold. - **Animating `height` instead of `transform`.** Layout props force a layout pass every frame; translating the header is cheaper. ## Compared with `Animated.event` Core `Animated.event` with `useNativeDriver: true` also keeps the offset off the JS thread, but it is a declarative mapping: you can interpolate the value, not run logic per event. `useAnimatedScrollHandler` runs code, so direction tracking, snapping and thresholds are straightforward.
- How would you snap a half-collapsed header fully open or closed when scrolling stops?Handle `onEndDrag` and `onMomentumEnd` in the same `useAnimatedScrollHandler`. If the offset rests inside the collapse range, scroll to the nearer edge with Reanimated's `scrollTo` on an animated ref, or, when the header has its own shared value, animate that with `withTiming`. Use `context` to remember state between the events.
- Your sticky header built with useAnimatedScrollHandler flickers while scrolling on the New Architecture. Where do you look?Reanimated's performance guide lists this case: on React Native 0.81 or newer, enable the `preventShadowTreeCommitExhaustion` React Native feature flag together with Reanimated's `DISABLE_COMMIT_PAUSING_MECHANISM` static flag. Also check the effect animates `transform` rather than layout props, and test a release build.
saying these in an interview costs you the question
- The drag and momentum handlers must be passed to onScrollBeginDrag and onMomentumScrollEnd props.
- useAnimatedScrollHandler works on a plain ScrollView imported from react-native.
- Each scroll event gets a fresh context object, so nothing carries between events.
- The scroll worklet can call setState directly because it receives a React event.
- useScrollViewOffset is still the current name in Reanimated 4.
- All five scroll events are delivered on the web as well.