skills/react-native-skills/rules/state-ground-truth.md
State variables—both React useState and Reanimated shared values—should
represent the actual state of something (e.g., pressed, progress, isOpen),
not derived visual values (e.g., scale, opacity, translateY). Derive
visual values from state using computation or interpolation.
Incorrect (storing the visual output):
const scale = useSharedValue(1)
const tap = Gesture.Tap()
.onBegin(() => {
scale.set(withTiming(0.95))
})
.onFinalize(() => {
scale.set(withTiming(1))
})
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.get() }],
}))
Correct (storing the state, deriving the visual):
const pressed = useSharedValue(0) // 0 = not pressed, 1 = pressed
const tap = Gesture.Tap()
.onBegin(() => {
pressed.set(withTiming(1))
})
.onFinalize(() => {
pressed.set(withTiming(0))
})
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],
}))
Why this matters:
State variables should represent real "state", not necessarily a desired end result.
pressed) describes what's
happening; visuals are derivedpressed = 1 is clearer than scale = 0.95pressed value can drive multiple visual
propertiesSame principle for React state:
// Incorrect: storing derived values
const [isExpanded, setIsExpanded] = useState(false)
const [height, setHeight] = useState(0)
useEffect(() => {
setHeight(isExpanded ? 200 : 0)
}, [isExpanded])
// Correct: derive from state
const [isExpanded, setIsExpanded] = useState(false)
const height = isExpanded ? 200 : 0
State is the minimal truth. Everything else is derived.