Moti vs. Reanimated

Should you use moti or react-native-reanimated? I get this question often, so let's break it down.

First off, Moti uses Reanimated under the hood. This means that Moti's components can do anything a Reanimated can, with additional features.

Simple comparison#

Let's start by comparing a simple example. Take this Moti component:

import { MotiView } from 'moti'
export function Moti({ isActive }) {
return <MotiView animate={{ opacity: isActive ? 1 : 0 }} />
}

Let's implement the same thing with Reanimated:

import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
export function Reanimated({ isActive }) {
const style = useAnimatedStyle(() => ({
opacity: withTiming(isActive ? 1 : 0),
}))
return <Animated.View style={style} />
}

Under the hood, Moti builds this useAnimatedStyle hook for you, with a number of additional features.

The benefit of using Reanimated directly is often more seen with imperative usage, which I'll touch on later. But for declarative styles, the Moti API usually offers what you need.

Reanimated props in Moti#

Let's rewrite the Reanimated example from above, this time using MotiView instead of Animated.View.

import { useAnimatedStyle, withTiming } from 'react-native-reanimated'
import { MotiView } from 'moti'
export function Reanimated({ isActive }) {
const style = useAnimatedStyle(() => ({
opacity: withTiming(isActive ? 1 : 0),
}))
return <MotiView style={style} />
}

Turns out, that's completely valid. After all, MotiView is a layer on top of Animated.View. If it works with Reanimated, then it works with Moti.

Shared Values#

Reanimated shared values let you power animations at 60 FPS without triggering any re-renders. This offers great performance.

When using useAnimationState or useDynamicAnimation from Moti, you are using a Reanimated shared value under the hood to power animations.

animate prop#

To use Reanimated shared values with Moti, you can pass useDerivedValue to the animate prop. This allows the animate prop to be fully reactive to shared value changes, without requiring re-renders.

import { MotiView } from 'moti'
import { useSharedValue, useDerivedValue } from 'react-native-reanimated'
export function WithSharedValue() {
const isValid = useSharedValue(false)
return (
<MotiView
animate={useDerivedValue(() => ({
opacity: isValid.value ? 1 : 0,
}))}
/>
)
}

The values in animate will automatically transition. You don't need to use withTiming/withSpring functions. Instead, you can customize transitions with the transition prop.

Derived values#

You can also use derived values with Moti. Here, we'll derive translateY from isValid. We'll then use it inside of the animate prop's derived value.

import { MotiView } from 'moti'
import { useSharedValue, useDerivedValue } from 'react-native-reanimated'
export function WithSharedValue() {
const isValid = useSharedValue(false)
const translateY = useDerivedValue(() => (isValid.value ? 0 : -10))
return (
<MotiView
animate={useDerivedValue(() => ({
opacity: isValid.value ? 1 : 0,
translateY: translateY.value,
}))}
transition={{
type: 'timing',
duration: 300,
translateY: {
// custom override for translateY
type: 'spring',
},
}}
/>
)
}

Custom transitions#

You can also pass useDerivedValue to your transition prop to use Reanimated values.

import { MotiView } from 'moti'
import { useSharedValue, useDerivedValue } from 'react-native-reanimated'
export function WithSharedValue() {
const isValid = useSharedValue(false)
return (
<MotiView
animate={useDerivedValue(() => ({
opacity: isValid.value ? 1 : 0,
}))}
transition={useDerivedValue(() => ({
delay: isValid.value ? 0 : 100,
}))}
/>
)
}

Gestures#

For animating based on simple interactions, such as hovered and pressed states, I recommend using moti/interactions.

However, Moti will also work with react-native-gesture-handler. You can use useSharedValue as shown above to track gestures.

Or, you can use a Moti hook:

import { GestureDetector, Gesture } from 'react-native-gesture-handler'
import { MotiView, useDynamicAnimation } from 'moti'
export function WithGestures() {
const state = useDynamicAnimation(() => ({
opacity: 0,
}))
const gesture = Gesture.Tap()
.onStart(() => {
state.animateTo({
opacity: 1,
})
})
.onEnd(() => {
state.animateTo({
opacity: 0,
})
})
return (
<GestureDetector gesture={gesture}>
<MotiView state={state} collapsable={false} />
</GestureDetector>
)
}

When should I use Reanimated directly?#

If you find yourself hacking together something really complicated with Moti, it might be worth trying out Reanimated directly. For complex gestures that require granular control, Reanimated is likely the way to go.

Imperative control#

Reanimated offers a cool way of using an imperative-style API:

const x = useSharedValue(0)
const y = useSharedValue(0)
const onPress = () => {
x.value = withTiming(20, { duration: 200 }, (finished) => {
if (finished) {
y.value = withSequence(withTiming(20), withSpring(30))
}
})
}

If you're chaining together multiple animations with complex things done in each step, Reanimated might be the way to go.

Moti lets you listen to changes in animations with the onDidAnimate prop, but it's harder to know which step of the animation was fired.

Direct comparisons#

Mount animations#

Moti#

import { MotiView } from 'moti'
export const Moti = () => (
<MotiView
from={{
translateY: -10,
opacity: 0,
}}
animate={{
translateY: 0,
opacity: 1,
}}
/>
)

Reanimated#

import Animated, {
useSharedValue,
withTiming,
withSpring,
useAnimatedStyle,
} from 'react-native-reanimated'
export const Reanimated = () => {
const isMounted = useSharedValue(false)
const style = useAnimatedStyle(() => {
return {
opacity: withTiming(isMounted.value ? 1 : 0),
transform: [
{
translateY: withSpring(isMounted.value ? 0 : -10),
},
],
}
})
useEffect(() => {
isMounted.value = true
}, [])
return <Animated.View style={style} />
}

Sequences#

Moti#

import { MotiView } from 'moti'
const Moti = () => (
<MotiView
animate={{
translateY: [
0,
10,
{ value: 0, delay: 100, type: 'timing', duration: 100 },
],
}}
/>
)

Reanimated#

import Animated, {
useAnimatedStyle,
withTiming,
withSpring,
withSequence,
} from 'react-native-reanimated'
export const Reanimated = () => {
const style = useAnimatedStyle(() => {
return {
transform: [
{
translateY: withSequence(
withSpring(0),
withSpring(10),
withTiming(0, { delay: 100, duration: 200 })
),
},
],
}
})
return <Animated.View style={style} />
}

Animation callbacks#

Moti#

import { MotiView } from 'moti'
const Moti = () => (
<MotiView
from={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
onDidAnimate={(key, finished, maybeValue, { attemptedValue }) => {
if (key === 'opacity') {
// do something
}
}}
/>
)

Reanimated#

import { useEffect } from 'react'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
export const Reanimated = () => {
const isMounted = useSharedValue(false)
const style = useAnimatedStyle(() => {
return {
opacity: withTiming(isMounted.value ? 1 : 0, undefined, (finished) => {
if (finished) {
// do something
}
}),
}
})
useEffect(() => {
isMounted.value = true
}, [])
return <Animated.View style={style} />
}