Reanimating Your React Native Experience

Authors
Raul Gomez Acuna
Software Engineer
@
Callstack

React Native is undergoing an outstanding transformation in the way gestures and animations are conceived. Thanks to the excellent work driven by Krzysztof Magiera, with react-native-gesture-handler and more recently with react-native-reanimated, we are one step closer to achieve the same delightful experiences as we’d have in native mobile apps.

Since a picture is worth a thousand words, take a look at the below GIF:

navigation bar in react native

In case you haven’t noticed, I’ve intentionally blocked the JS thread and everything runs smoothly, even the snapping animation after the finger release. Say whaaat?

Your mileage may vary with Animations in React Native, but the experts will agree that I’ve either cheated or hacked the RN profiler, because they know that can’t be achieved with the current built in infrastructure (aka <rte-code>Animated<rte-code>API)…until now.

If you are super impatient, you can check out the source on GitHub. I still recommend you continue reading though.

React-native-reanimated

Animated library has several limitations that become troubling when it comes to gesture-based interactions. When dragging a box, even though by using <rte-code>Animated.event<rte-code> we could map gesture state to the position of the box and make this whole interaction run on UI thread with <rte-code>useNativeDriver<rte-code> flag, we still have to call back into JS at the end of the gesture for us to start a "snap" animation. That’s because <rte-code>Animated.spring({}).start()<rte-code> cannot be used in a "declarative" manner.

This extract from the README file of the library greatly sums up one important pain point when building gesture based interactions with React Native.

Think about the common collapsible navbar pattern used across different popular apps, such as Twitter or Whatsapp. To implement snapping, we had to do a lot of manual work, due to problems like Animated.diffClamp not supporting adding listeners to it and having to resort to hacky solutions that led to performance issues and more often than not, brittle code.

React-native-reanimated is a (backwards compatible) reimplementation of Animated API, whose goal is to provide us with more generic and low level primitive node types, so that patterns such as the one above can be implemented declaratively and run on the UI thread like a sweet piece of cheesecake.

Ok, enough theory. Let’s reveal the secret sauce I’ve put together to implement the fancy collapsible navigation bar you saw on the GIF above.

Hiding navigation bar with diffClamp

The first part of the implementation consists of showing or hiding the navigation bar, depending on the direction and amount of scroll. It’s certainly not a new pattern, since it’s been explained before in other blog posts and code samples. Still, let’s do a quick refresher.

<rte-code>Animated.diffclamp<rte-code> is the method that allows us to represent the desired behavior. It calculates the difference between the current value and the last and then clamp that value. To better illustrate what it does, let’s throw a table with some numbers. What you see below is the output for <rte-code>diffClamp(0, 20)<rte-code>.

input | output
0 | 0
20 | 20
40 | 20
60 | 20
50 | 10
40 | 0
50 | 10
20 | 0

Assuming you got a better understanding, let’s scaffold the UI and hook up the corresponding animated values to make that happen. I’ll be omitting some of the code parts for selectively showing what matters. You’ll be able to check the full source code later on.

[...]
import Animated, {
diffClamp,
interpolate,
event,
multiply
} from 'react-native-reanimated';
class CollapsibleNavBar extends React.Component {
constructor(props) {
super(props);
this.scrollY = new Value(0);
const diffClampNode = diffClamp(this.scrollY, 0, NAV_BAR_HEIGHT);
this.animatedNavBarTranslateY = multiply(diffClampNode, -1);
this.animatedTitleOpacity = interpolate(this.animatedNavBarTranslateY, {
inputRange: [-NAV_BAR_HEIGHT, 0],
outputRange: [0, 1],
extrapolate: 'clamp',
});
}
render() {
return (
<View style={styles.container}>
<Animated.ScrollView
scrollEventThrottle={1}
onScroll={event(
[
{
nativeEvent: {
contentOffset: {
y: this.scrollY,
},
},
},
],
{ useNativeDriver: true },
)}
>
{Array.from({ length: 60 }).map((_, i) => (
<View key={i} style={styles.row}>
<Text>{i}</Text>
</View>
))}
</Animated.ScrollView>
<Animated.View
style={[
styles.navBar,
{
transform: [
{
translateY: this.animatedNavBarTranslateY,
},
],
},
]}
>
<Animated.Text
style={[
styles.navBarTitle,
{ opacity: this.animatedTitleOpacity },
]}
>
Navigation Bar
</Animated.Text>
</Animated.View>
</View>
);
}
}
[...]

Let’s explain what’s going on in the constructor:

  • <rte-code>this.scrollY<rte-code> is the animated value that will be driven by the ScrollView <rte-code>contentOffset.y<rte-code>. The mapping is performed by using <rte-code>Animated.Event<rte-code>. The native driver is running the animation on the UI thread, so we are not affected by the JS thread being blocked.
  • <rte-code>diffClampNode<rte-code> represents the <rte-code>Animated.diffClamp(0, NAV_BAR_HEIGHT)<rte-code> operation explained before.
  • Since we want to hide the navigation bar when scrolling down and show it when scrolling back up, we define <rte-code>animatedNavBarTranslateY<rte-code> as the transformY style applied to it, by inverting the relationship.
  • We interpolate the opacity of the title, so that the title is visible when the navigation bar is visible and viceversa.

New concepts

So far so good. We’ve got the barebones of our implementation up and running. Now it’s time to show off some magic. But before that, I’ll kindly introduce some new concepts that react-native-reanimated embraces, so that you can have the big picture.

Clocks

Clocks aim to replace animated objects by providing a more low level abstraction, still behaving as animated values. Animated.Clock nodes are a special type of <rte-code>Animated.Value<rte-code> that can be updated in each frame to the timestamp of the current frame. They are also denoted as side effect nodes, since they are in charge of starting and stopping a process (an animation) that updates the value for some time.

The algorithm that evaluates animated nodes works as follows:

  • Each frame it analyses first the generated events (e.g. touch stream), because they may update some animated values.
  • Then, it updates values that corresponds to clock nodes that are “running”.
  • After that, it recursively and efficiently evaluates nodes that are connected to views (and that have to be updated in the current frame).
  • Finally, it checks if some “running” clocks exist. If so, it enqueues a callback to be evaluated with the next frame.

Blocks

A block is just an array of nodes, where each node is evaluated in order. It returns the value of the last node.

If those terms sound confusing for now, don’t worry. I am aware it’s difficult to assimilate them at first, by using just words. We’ll soon refer back to those concepts when getting our hands dirty with more code.

Snapping

It’s time to tackle the 2nd part of the implementation, which is the snapping part.

Detecting the end of scrolling

First, we need to figure out how to detect that we’ve finished scrolling. There are 2 callbacks provided by the <rte-code>ScrollView<rte-code> component that can serve as hooks for that, onScrollEndDrag and onMomentumScrollEnd.

<rte-code>OnMomentumScrollEnd<rte-code> will be called only if we release the finger with certain inertia, whereas <rte-code>onScrollEndDrag<rte-code> will be always called after the end of the gesture. For simplicity, we will focus on leveraging <rte-code>onScrollEndDrag<rte-code>.

Following a similar approach as with the <rte-code>onScroll<rte-code> prop, we can use <rte-code>Animated.Event<rte-code> to map the native event <rte-code>velocity.y<rte-code> to an animated value, that we’ll call <rte-code>scrollEndDragVelocity<rte-code>, and use the native driver.

Android and iOS both differ in the native implementation of the <rte-code>onScrollEndDrag<rte-code> callback, bridging inconsistent values for velocities when the callback is executed on the JS realm. iOS reports a velocity of <rte-code>0<rte-code>, whilst Android shows a very low value for velocity, but different than <rte-code>0<rte-code>.

To circumvent that, we can initialize <rte-code>scrollEndDragVelocity<rte-code> with a very high numerical value and listen for changes, so we’ll know we’ve ended the scrolling gesture when we get a value different than the default one.

With that in mind, we can tweak our previous <rte-code>animatedNavBarTranslateY<rte-code> definition as follows:

import Animated from 'react-native-reanimated';
const {
event,
Value,
diffClamp,
multiply,
interpolate,
cond,
neq,
} = Animated;
const DRAG_END_INITIAL = 10000000;
class CollapsibleNavBar extends React.Component {
constructor(props) {
super(props);
this.scrollY = new Value(0);
this.scrollEndDragVelocity = new Value(DRAG_END_INITIAL);
const diffClampNode = diffClamp(this.scrollY, 0, NAV_BAR_HEIGHT);
this.animatedNavBarTranslateY = cond(
// Condition to detect if we stopped scrolling
neq(this.scrollEndDragVelocity, DRAG_END_INITIAL),
[], // It's driven by snapping animation (To be implemented)
multiply(diffClampNode, -1), // Otherwise it's driven by scrolling
);
this.animatedTitleOpacity = interpolate(this.animatedNavBarTranslateY, {
inputRange: [-NAV_BAR_HEIGHT, 0],
outputRange: [0, 1],
extrapolate: 'clamp',
});
}
render() {
return (
<View style={styles.container}>
<Animated.ScrollView
scrollEventThrottle={1}
onScroll={event(
[
{
nativeEvent: {
contentOffset: {
y: this.scrollY,
},
},
},
],
{ useNativeDriver: true },
)}
onScrollEndDrag={event(
[
{
nativeEvent: {
velocity: {
y: this.scrollEndDragVelocity,
},
},
},
],
{ useNativeDriver: true },
)}
>
{Array.from({ length: 60 }).map((_, i) => (
<View key={i} style={styles.row}>
<Text>{i}</Text>
</View>
))}
</Animated.ScrollView>
[...]
</View>
);
}
}

Snap threshold

Next piece of the puzzle is to determine which final position the navigation bar should animate to, after the scroll is over. We’ll set the threshold on the value <rte-code>NAV_BAR_HEIGHT / 2<rte-code>. The snapping point is then defined as:

import Animated from 'react-native-reanimated';
const {
event,
Value,
diffClamp,
multiply,
interpolate,
cond,
neq,
} = Animated;
const DRAG_END_INITIAL = 10000000;
class CollapsibleNavBar extends React.Component {
constructor(props) {
super(props);
this.scrollY = new Value(0);
this.scrollEndDragVelocity = new Value(DRAG_END_INITIAL);
const diffClampNode = diffClamp(this.scrollY, 0, NAV_BAR_HEIGHT);
this.animatedNavBarTranslateY = cond(
// Condition to detect if we stopped scrolling
neq(this.scrollEndDragVelocity, DRAG_END_INITIAL),
[], // It's driven by snapping animation (To be implemented)
multiply(diffClampNode, -1), // Otherwise it's driven by scrolling
);
this.animatedTitleOpacity = interpolate(this.animatedNavBarTranslateY, {
inputRange: [-NAV_BAR_HEIGHT, 0],
outputRange: [0, 1],
extrapolate: 'clamp',
});
}
render() {
return (
<View style={styles.container}>
<Animated.ScrollView
scrollEventThrottle={1}
onScroll={event(
[
{
nativeEvent: {
contentOffset: {
y: this.scrollY,
},
},
},
],
{ useNativeDriver: true },
)}
onScrollEndDrag={event(
[
{
nativeEvent: {
velocity: {
y: this.scrollEndDragVelocity,
},
},
},
],
{ useNativeDriver: true },
)}
>
{Array.from({ length: 60 }).map((_, i) => (
<View key={i} style={styles.row}>
<Text>{i}</Text>
</View>
))}
</Animated.ScrollView>
[...]
</View>
);
}
}

Running the animation

After that, we have to create a function that will run the animation after scrolling. We’ll use a spring-based animation.

[...]
function runSpring({
clock, // The clock instance
from, // Initial value before starting the animation
velocity, // Initial velocity of the spring animation
toValue, // Final value of the animation
scrollEndDragVelocity,
}) {
const state = {
finished: new Value(0),
velocity: new Value(0),
position: new Value(0),
time: new Value(0),
};
const config = {
damping: 1,
mass: 1,
stiffness: 50,
overshootClamping: true,
restSpeedThreshold: 0.001,
restDisplacementThreshold: 0.001,
toValue: new Value(0),
};
return [
cond(clockRunning(clock), 0, [
set(state.finished, 0),
set(state.velocity, velocity),
set(state.position, from),
set(config.toValue, toValue),
startClock(clock),
]),
spring(clock, state, config),
cond(state.finished, [
// Once the animation is done, we reset scrollEndDragVelocity to its default value
set(scrollEndDragVelocity, DRAG_END_INITIAL),
stopClock(clock),
]),
state.position,
];
}
class CollapsibleNavBar extends React.Component {
constructor(props) {
super(props);
this.scrollY = new Value(0);
this.scrollEndDragVelocity = new Value(DRAG_END_INITIAL);
const diffClampNode = diffClamp(this.scrollY, 0, NAV_BAR_HEIGHT);
const inverseDiffClampNode = multiply(diffClampNode, -1);
const snapPoint = cond(
lessThan(diffClampNode, NAV_BAR_HEIGHT / 2),
0,
-NAV_BAR_HEIGHT,
);
const clock = new Clock();
this.animatedNavBarTranslateY = cond(
// Condition to detect if we stopped scrolling
neq(this.scrollEndDragVelocity, DRAG_END_INITIAL),
runSpring(
clock,
from: inverseDiffClampNode,
velocity: 0,
toValue: snapPoint,
scrollEndDragVelocity: this.scrollEndDragVelocity,
),
inverseDiffClampNode,
);
[...]
}
[...]
}
view raw runSpring1.js hosted with ❤ by GitHub

Now, let’s take a look at how <rte-code>this.animatedNavBarTranslateY<rte-code> is redefined. If the scrolling is over, <rte-code>neq(this.scrollEndDragVelocity, DRAG_END_INITIAL)<rte-code> will evaluate to <rte-code>true<rte-code>. Hence, the <rte-code>cond<rte-code> node will evaluate <rte-code>runSpring<rte-code> and return its value, which will be assigned to <rte-code>this.animatedNavBarTranslateY<rte-code>.

In other words, <rte-code>this.animatedNavBarTranslateY<rte-code> will be driven by the spring animation and not by the scroll <rte-code>contentOffset.y<rte-code> value at that point.

Remember when we talked about clocks and blocks? Now we’ll see them in practice. Let’s go straight to <rte-code>runSpring<rte-code> return value to see how it works.

return [
cond(clockRunning(clock), 0, [
set(state.finished, 0),
set(state.velocity, velocity),
set(state.position, from),
set(config.toValue, toValue),
startClock(clock),
]),
spring(clock, state, config),
cond(state.finished, [
set(scrollEndDragVelocity, DRAG_END_INITIAL),
stopClock(clock),
]),
state.position,
];
view raw runSpring.js hosted with ❤ by GitHub

The 1st time we call the function, <rte-code>clockRunning(clock)<rte-code> will evaluate to <rte-code>false<rte-code> because the clock node has not been started, so the 3rd argument of the <rte-code>cond<rte-code> node will be evaluated. Since that argument is a block, we evaluate all the nodes in order (which set up the initial state and configuration of the spring animation) and return the value of the last one, which has the side effect of starting a clock node.

<rte-code>spring(clock, state, config)<rte-code> will calculate the position of the animation for the current frame and update <rte-code>state.position<rte-code> accordingly. The next <rte-code>cond<rte-code> will only evaluate if the animation is done, so that we can reset <rte-code>scrollEndDragVelocity<rte-code> and stop the clock.

Finally, we return <rte-code>state.position<rte-code> to the caller, which ends up assigning that value to <rte-code>this.animatedNavBarTranslateY.<rte-code>

If you recall the last step of the react-native-reanimated algorithm, we have a clock running, so a callback is enqueued to the next frame. That will have the effect of going through the block repeatedly, until the clock is stopped, which will occur after the animation finishes.

I am getting quite into details here, but I am doing that so that you can acquire the mental model that react-native-reanimated uses.

Crossing the last mile

We are getting there, but we are still missing one subtle detail. After the snapping finishes, the next time we scroll again we’ll get some weird behavior.

That’s why as soon as we interact with the <rte-code>ScrollView<rte-code> by dragging, <rte-code>this.animatedNavBarTranslateY<rte-code> will be driven again by <rte-code>multiply(diffClamp(0, NAV_BAR), -1)<rte-code>, which was unaware of the amount applied by the snapping mechanism. The table below illustrates the 2 different scenarios we can run into.

Let's assume our navigation bar has a height of 80, so it's driven by diffClamp(0, 80)
The position of the navigation bar is dictated by the inverse of that operation.
navBarTranslateY = multiply(diffClamp(0, 80), -1);
# Scenario 1
scrollY | navBarY | snapAmount
0 | 0 |
20 | -20 |
30 | -30 |
Release | * | 30
* | 0 |
30 | -30 | <- We scroll again. NavBar position changes abruptly from 0 to -30
50 | -50 |
# Scenario 2
scrollY | navBarY | snapAmount
0 | 0 |
80 | -80 |
60 | -60 |
Release | * | -20
* | -80 |
60 | -60 | <- We scroll again. NavBar position changes abruptly from -80 to -60
0 | 0 |
view raw offsetTable hosted with ❤ by GitHub

To coordinate the two agents that are able to drive the navigation bar position, we can use a new animated value that will be in charge of compensating the amount applied by snapping. We’ll call it <rte-code>snapOffset<rte-code>.

Let’s redefine <rte-code>diffClampNode<rte-code> to account for this new variable:

this.snapOffset = new Value(0);
const diffClampNode = diffClamp(
add(this.scrollY, this.snapOffset),
0,
NAV_BAR_HEIGHT,
);
const inverseDiffClampNode = multiply(diffClampNode, -1);
view raw diffClampNode hosted with ❤ by GitHub

Once our <rte-code>runSpring<rte-code> function completes the animation, <rte-code>state.finished<rte-code> will evaluate to <rte-code>true<rte-code>. At this point, besides reseting <rte-code>scrollEndDragVelocityanimated<rte-code> value, it also needs to apply the right amount to <rte-code>snapOffset<rte-code> , depending on the point we are snapping to.

return [
cond(clockRunning(clock), 0, [
set(state.finished, 0),
set(state.velocity, velocity),
set(state.position, from),
set(config.toValue, toValue),
startClock(clock),
]),
spring(clock, state, config),
cond(state.finished, [
set(scrollEndDragVelocity, DRAG_END_INITIAL),
set(
snapOffset,
cond(
eq(toValue, 0),
// SnapOffset acts as an accumulator.
// We need to keep track of the previous offsets applied.
add(snapOffset, multiply(diffClampNode, -1)),
add(snapOffset, sub(NAV_BAR_HEIGHT, diffClampNode)),
),
),
stopClock(clock),
]),
state.position,
];
view raw snapOffset hosted with ❤ by GitHub

And that’s it! We’ve finally got everything right in place. If you want to check the nodes API in details, you can do it by reading the GitHub documentation. Putting all together:

[...]
function runSpring({
clock,
from,
velocity,
toValue,
scrollEndDragVelocity,
snapOffset,
diffClampNode,
}) {
const state = {
finished: new Value(0),
velocity: new Value(0),
position: new Value(0),
time: new Value(0),
};
const config = {
damping: 1,
mass: 1,
stiffness: 50,
overshootClamping: true,
restSpeedThreshold: 0.001,
restDisplacementThreshold: 0.001,
toValue: new Value(0),
};
return [
cond(clockRunning(clock), 0, [
set(state.finished, 0),
set(state.velocity, velocity),
set(state.position, from),
set(config.toValue, toValue),
startClock(clock),
]),
spring(clock, state, config),
cond(state.finished, [
set(scrollEndDragVelocity, DRAG_END_INITIAL),
set(
snapOffset,
cond(
eq(toValue, 0),
// SnapOffset acts as an accumulator.
// We need to keep track of the previous offsets applied.
add(snapOffset, multiply(diffClampNode, -1)),
add(snapOffset, sub(NAV_BAR_HEIGHT, diffClampNode)),
),
),
stopClock(clock),
]),
state.position,
];
}
class CollapsibleNavBar extends React.Component {
constructor(props) {
super(props);
this.scrollY = new Value(0);
this.scrollEndDragVelocity = new Value(DRAG_END_INITIAL);
this.snapOffset = new Value(0);
const diffClampNode = diffClamp(
// snapOffset will compensate the value applied by snapping to avoid glitches
add(this.scrollY, this.snapOffset),
0,
NAV_BAR_HEIGHT,
);
const inverseDiffClampNode = multiply(diffClampNode, -1);
const clock = new Clock();
const snapPoint = cond(
lessThan(diffClampNode, NAV_BAR_HEIGHT / 2),
0,
-NAV_BAR_HEIGHT,
);
this.animatedNavBarTranslateY = cond(
// Condition to detect if we stopped scrolling
neq(this.scrollEndDragVelocity, DRAG_END_INITIAL),
runSpring({
clock,
from: inverseDiffClampNode,
velocity: 0,
toValue: snapPoint,
scrollEndDragVelocity: this.scrollEndDragVelocity,
snapOffset: this.snapOffset,
diffClampNode,
}),
inverseDiffClampNode,
);
this.animatedTitleOpacity = interpolate(this.animatedNavBarTranslateY, {
inputRange: [-NAV_BAR_HEIGHT, 0],
outputRange: [0, 1],
extrapolate: 'clamp',
});
}
render() {
return (
<View style={styles.container}>
<Animated.ScrollView
bounces={false}
scrollEventThrottle={1}
onScroll={event(
[
{
nativeEvent: {
contentOffset: {
y: this.scrollY,
},
},
},
],
{ useNativeDriver: true },
)}
onScrollEndDrag={event(
[
{
nativeEvent: {
velocity: {
y: this.scrollEndDragVelocity,
},
},
},
],
{ useNativeDriver: true },
)}
>
{Array.from({ length: 60 }).map((_, i) => (
<View key={i} style={styles.row}>
<Text>{i}</Text>
</View>
))}
</Animated.ScrollView>
<Animated.View
style={[
styles.navBar,
{
transform: [
{
translateY: this.animatedNavBarTranslateY,
},
],
},
]}
>
<Animated.Text
style={[styles.navBarTitle, { opacity: this.animatedTitleOpacity }]}
>
Navigation Bar
</Animated.Text>
</Animated.View>
</View>
);
}
}
view raw runSpring2.js hosted with ❤ by GitHub

Summary

If you’ve managed to read up until this line, give yourself 10 declarative points!

Because being a declarative citizen is what is all about. We’ve defined all the animation system constraints in the component constructor and send them off to the native thread. No more passes over the bridge were needed.Therefore, we are free to carry out whatever heavy computation on the JS thread, or run an infinite loop (just kidding, don’t do that my dear reader), because we have full guarantees that the animation will run slick.

Last but not least, if you wanna play around with the code, here is the repository on GitHub.

Happy coding!

Insights

Learn more about Animations

cover

Jul 8Podcast

React Native Ease: Platform-Native Animations Without the Extra Work

Łukasz talks with Janic from App & Flow about React Native Ease, Core Animation, Reanimated trade-offs, and performant UI animation in React Native.

cover

Jul 7Podcast

Where Reanimated Goes Next

A conversation with Bartłomiej Błoniarz on CSS animations in Reanimated, native primitives on iOS, shared element transitions, and the work still left on stability.

cover

Oct 30Live Stream

Reanimated, TanStack, Cross-Pollinating Languages & TWIR #256 Anniversary

Catch up on everything from TanStack updates and Astro insights to Reanimated deep dive and even Swift for Android. Plus, we celebrate the 256th newsletter!

Events Cover

Apr 16Talk

How to Make Your React (Native) App Feel Native

How to make your React Native app feel truly native, across iOS, Android, tvOS & more. Dive into best practices, navigation, and react-native-bottom-tabs.

cover

Feb 19Podcast

Reanimated 4 is the Future of Smooth React Native Animations

Reanimated 4 is now available in beta, and if you work with animations in React Native, you need to know what’s new!

cover

Oct 11Article

A Deep Dive Into React Native Reanimated v1

Read the article to learn more about what React Native Reanimated is, how it works under the hood, and what idea stands behind the library.

cover

Apr 15Article

ReanimatedArc - Build Circular Animated UI Elements in React Native

Build good-looking and smooth animated arcs in React Native with ReanimatedArc library.

cover

Oct 25Article

Custom Screen Transitions in React Navigation

Wondering how to custom a screen transition animation in React Navigation? Check this step-by-step guide.

cover

Jan 17Article

React Native Animations Revisited - Part III

Learn how to respond to screen gestures with PanResponder. Check the last part of our Callstack series on React Native animations!

cover

Jan 10Article

React Native Animations Revisited - Part II

In this part of the three-part guide about animations in React Native we cover creating animations with Animated API.

cover

Jan 4Article

React Native Animations Revisited - Part I

Three-part guide for developers explaining how to deal with animations in React Native - Part 1.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.