skip to content

In Reanimated 4, how do useSharedValue and useAnimatedStyle move a view on every frame without re-rendering the React component?

level: middleimportance: must knowfreq 62%

answer

  1. a mutable box, not React state
  2. read and write through .value or get/set
  3. updater becomes a UI-runtime worklet
  4. re-runs when a shared value it reads changes
  5. only Animated components accept the result

basics

~20 s

A shared value is a mutable container kept in sync between the JS and UI runtimes. useAnimatedStyle's updater is a UI-runtime worklet that re-runs when a shared value it reads changes, so view props update without a React render.

solid answer

~40 s

`useSharedValue(initial)` returns a plain object whose `.value` (or `get()`/`set()`) Reanimated keeps in sync between the React Native runtime and the UI runtime; writing it does not schedule a React render. `useAnimatedStyle(() => ({ transform: [{ translateY: y.value }] }))` turns its updater into a worklet that runs on the UI thread and re-runs whenever a shared value it read changes, applying the result to the native view of an `Animated.View`. Assigning `withTiming` or `withSpring` to `.value` makes that happen every frame. The rules follow from the model: don't touch `.value` during render, reassign objects instead of mutating their fields, don't destructure `.value`, don't write shared values inside the updater, and use `useDerivedValue` for values computed from other shared values.

code

tsx · 49 lines
tsx
import { Pressable, StyleSheet, View } from 'react-native';
import Animated, {
  Extrapolation,
  interpolate,
  useAnimatedStyle,
  useDerivedValue,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';

const PEEK_Y = 420;

export function PlaceSheet() {
  const sheetY = useSharedValue(PEEK_Y);

  const backdropOpacity = useDerivedValue(() =>
    interpolate(sheetY.value, [0, PEEK_Y], [0.4, 0], Extrapolation.CLAMP),
  );

  const sheetStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: sheetY.value }],
  }));
  const backdropStyle = useAnimatedStyle(() => ({
    opacity: backdropOpacity.value,
  }));

  const expand = () => {
    sheetY.value = withSpring(0);
  };
  const collapse = () => {
    sheetY.value = withSpring(PEEK_Y);
  };

  return (
    <View style={StyleSheet.absoluteFill}>
      <Animated.View style={[StyleSheet.absoluteFill, styles.backdrop, backdropStyle]} />
      <Animated.View style={[styles.sheet, sheetStyle]}>
        <Pressable accessibilityRole="button" accessibilityLabel="Expand place details" onPress={expand} style={styles.handle} />
        <Pressable accessibilityRole="button" accessibilityLabel="Collapse place details" onPress={collapse} style={styles.handle} />
      </Animated.View>
    </View>
  );
}

const styles = StyleSheet.create({
  backdrop: { backgroundColor: 'black', pointerEvents: 'none' },
  sheet: { position: 'absolute', left: 0, right: 0, top: 80, bottom: 0, backgroundColor: 'white' },
  handle: { alignSelf: 'center', width: 48, height: 24, marginTop: 8 },
});

go deeper

for a junior

Recall the pair: useSharedValue holds a value both threads can see, and useAnimatedStyle turns it into a style for an Animated component. Say clearly that changing it does not re-render.

for a middle

Explain that the updater is workletized, runs on the UI runtime and re-runs when shared values it reads change, and list the reactivity traps: object mutation, destructuring and reading during render.

for a senior

Show you keep per-frame data out of React state, derive values with useDerivedValue, animate transform and opacity rather than layout props, and avoid synchronous JS-thread reads that stall on a busy UI thread.

for a principal

Frame it as a boundary decision: which state lives in shared values on the UI runtime and which in React, and how the team keeps the two from drifting as screens grow.

## The problem shared values solve In React Native your components render on the **JS thread**, while the native views are drawn on the **UI (main) thread**. If an animation stored its current position in React state, every frame would need a `setState`, a render, a reconciliation and a commit, all on a JS thread that is also busy with data loading and business logic. A dragged bottom sheet over a map would stutter the moment the JS thread fell behind. Reanimated 4 avoids that round trip. It runs small JavaScript functions called **worklets** on a separate **UI runtime** driven by the UI thread, and it gives those worklets a way to share data with your component: the **shared value**. ## `useSharedValue`: a box both runtimes can see `const sheetY = useSharedValue(420)` returns an object, not a number. You read and write it through `sheetY.value`, or through `sheetY.get()` and `sheetY.set(...)` (the forms to use when the React Compiler is on). - **Writes never re-render.** A shared value is a plain JavaScript object, so changing it does not schedule a React render. Reanimated instead notifies the worklets that read it. - **Any JS value fits**: numbers, strings, booleans, arrays and objects. Functions (including worklets) must not be stored in one. - **Animations are assigned, not awaited.** `sheetY.value = withSpring(0)` stores an *animation object*; Reanimated advances it frame by frame on the UI runtime, and the shared value holds a plain number at each step. - **Initial value can be lazy**: `useSharedValue(() => computeStart())` calls the function once on mount. ## `useAnimatedStyle`: a worklet that writes view props `useAnimatedStyle(updater)` returns a style object that must be passed to the `style` prop of an **Animated component** (`Animated.View`, `Animated.Text`, `Animated.ScrollView`, or one made with `createAnimatedComponent`). Passing it to a plain `View` is an error. What happens on a frame where `sheetY` moves: 1. The spring (or a gesture callback) writes a new number into `sheetY` on the UI runtime. 2. Reanimated re-runs every updater that read `sheetY.value`. 3. The returned props are applied to the native view; no JS-thread render takes place. The updater is **workletized** automatically by the Worklets Babel plugin, so you do not write a `'worklet'` directive in it. It is first run on the JS thread and then on the UI thread, so code that assumes it only ever runs on the UI runtime can fail; the docs suggest checking `global._WORKLET` for UI-only branches. It also re-runs when React state it closes over changes. ## Deriving values with `useDerivedValue` When several styles depend on one computed number, compute it once. `useDerivedValue(() => interpolate(sheetY.value, [0, 420], [0.4, 0], Extrapolation.CLAMP))` returns a **read-only** shared value whose updater also runs on the UI runtime and re-runs when its inputs change. A map screen can drive both the sheet's `translateY` and the dimming backdrop's `opacity` from the one `sheetY`, with no React state involved. If you need the previous value too, `useAnimatedReaction` is the hook that provides it. ## Rules that fall out of the model | Mistake | What happens | Do instead | |---|---|---| | Reading or writing `.value` during render | A side effect in render, against the Rules of React | Access it in `useAnimatedStyle`, event handlers or `useEffect` | | `pos.value.x = 50` | Reanimated loses reactivity; nothing updates | `pos.value = { x: 50, y: 0 }` or `pos.modify(...)` | | `let { value } = pos` | The local copy is not reactive | Always go through `pos.value` | | Writing a shared value inside the updater | Undefined behaviour, possibly an infinite loop | Write in handlers, gesture callbacks or effects | | Reading `.value` often on the JS thread | JS blocks until the UI thread returns the value | Read it in worklets | Three more behaviours surprise people: - **Animated styles win over static ones regardless of array order.** In `style={[animatedStyle, { width: 100 }]}` the animated `width` applies. Among animated styles, the last one updated takes effect. - **Removing an animated style does not reset what it applied.** Set the props back to `undefined` in the updater if you need them cleared. - **A write from the JS thread is not visible to an immediate JS read**: the docs' own example logs the old value right after `sv.value += 50`. ## Why this is faster Keep static styles in `StyleSheet` and only the moving part in `useAnimatedStyle`, so the updater does as little as possible per frame. Prefer non-layout props such as `transform` and `opacity` over `top` or `height`, because layout props force a layout pass on every frame. The payoff is that the sheet keeps following the finger even while the JS thread is busy fetching place details.

  • Why is reading a shared value's .value inside a useEffect on the JS thread discouraged?
    Reading `.value` on the React Native runtime blocks the JS thread until the value is fetched from the UI thread. Usually that is negligible, but when the UI thread is busy or you read repeatedly, the wait grows. Read shared values inside worklets such as `useAnimatedStyle`, and push a value to React only at discrete moments.
  • When would you reach for useDerivedValue instead of computing inside useAnimatedStyle?
    When a computed value feeds more than one style or another worklet, such as a backdrop opacity and a handle rotation both derived from the sheet's offset. `useDerivedValue` runs its updater on the UI runtime, re-runs it when its inputs change and returns a read-only shared value. If you also need the previous value, use `useAnimatedReaction`.
  • What changes about shared value access when the React Compiler is enabled?
    Reanimated asks you to stop reading and assigning `.value` directly and use `sv.get()` and `sv.set(v)` (or `sv.set(prev => next)`) instead. They are the same operations exposed as methods, which the compiler's rules accept.

saying these in an interview costs you the question

  • Changing a shared value re-renders the component the way setState does.
  • An object returned by useAnimatedStyle can be passed to a plain View from react-native.
  • Assigning sv.value.x = 50 on an object shared value keeps the style reactive.
  • Reading sv.value on the JS thread is free because it is just a field.
  • The last entry in a style array always overrides an animated style.
  • Writing another shared value inside the useAnimatedStyle updater is a safe way to chain animations.