---
title: Never Track Scroll Position in useState
impact: HIGH
impactDescription: prevents render thrashing during scroll
tags: scroll, performance, reanimated, useRef
---

## Never Track Scroll Position in useState

Never store scroll position in `useState`. Scroll events fire rapidly—state
updates cause render thrashing and dropped frames. Use a Reanimated shared value
for animations or a ref for non-reactive tracking.

**Incorrect (useState causes jank):**

```tsx
import { useState } from 'react';
import { ScrollView, NativeSyntheticEvent, NativeScrollEvent } from 'react-native';

function Feed() {
  const [scrollY, setScrollY] = useState(0);

  const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
    setScrollY(e.nativeEvent.contentOffset.y); // re-renders on every frame
  };

  return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />;
}
```

**Correct (Reanimated for animations):**

```tsx
import Animated, { useSharedValue, useAnimatedScrollHandler } from 'react-native-reanimated';

function Feed() {
  const scrollY = useSharedValue(0);

  const onScroll = useAnimatedScrollHandler({
    onScroll: (e) => {
      scrollY.value = e.contentOffset.y; // runs on UI thread, no re-render
    },
  });

  return (
    <Animated.ScrollView
      onScroll={onScroll}
      // higher number has better performance, but it fires less often.
      // unset this if you need higher precision over performance.
      scrollEventThrottle={16}
    />
  );
}
```

**Correct (ref for non-reactive tracking):**

```tsx
import { useRef } from 'react';
import { ScrollView, NativeSyntheticEvent, NativeScrollEvent } from 'react-native';

function Feed() {
  const scrollY = useRef(0);

  const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
    scrollY.current = e.nativeEvent.contentOffset.y; // no re-render
  };

  return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />;
}
```
