In a world filled with constant notifications and distractions, taking a moment to focus on our breath can be a powerful tool for grounding ourselves. Mindful breathing exercises have been shown to reduce stress and improve focus, making them a popular feature in wellness and mental health apps.
This tutorial will guide you through building a simple yet elegant mindful breathing exercise app using React Native and the powerful react-native-reanimated library. We'll focus on creating a visually calming and responsive user experience with smooth animations that are crucial for an app of this nature. A jittery or unresponsive animation can defeat the purpose of a relaxation app, which is why Reanimated is the perfect tool for this job. It allows us to run our animations on the native UI thread, bypassing the JavaScript bridge to achieve buttery-smooth, 60 FPS animations.
By the end of this tutorial, you'll have a functional breathing app and a solid understanding of how to leverage Reanimated to create high-performance animations in your own React Native projects.
Prerequisites
- A working knowledge of React Native and JavaScript.
- Node.js and npm/yarn installed on your machine.
- A React Native development environment set up (Expo Go or React Native CLI).
- Familiarity with React Hooks.
Understanding the Problem
The core challenge in creating a breathing app is to provide clear, calming visual feedback to the user, guiding them through the breathing cycle (inhale, hold, exhale). This requires:
- A smooth, continuous animation: A circle that expands and contracts to represent breathing in and out.
- Synchronized text instructions: Text that updates in time with the animation to guide the user.
- A distraction-free UI: A minimalist design that keeps the user focused on the exercise.
React Native's built-in Animated API can be a good starting point for animations, but for gesture-based interactions and animations that need to be highly performant, react-native-reanimated is the superior choice. It provides a more declarative API and runs animations on the UI thread, preventing them from being interrupted by JavaScript thread operations.
Prerequisites
Before we start, make sure you have a new React Native project. We'll be using Expo for this tutorial for a quicker setup.
npx create-expo-app mindful-breathing-app
cd mindful-breathing-app
Next, install react-native-reanimated:
npx expo install react-native-reanimated
Finally, add the reanimated plugin to your babel.config.js:
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'],
};
};
Now, we're ready to start building!
Step 1: Building the Basic UI
First, let's create the basic visual elements: a circle that will expand and contract, and a text element for instructions.
What we're doing
We'll create a simple, centered layout with a circle and a text component.
Implementation
// App.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<View style={styles.circle}>
<Text style={styles.text}>Breathe</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
alignItems: 'center',
justifyContent: 'center',
},
circle: {
width: 200,
height: 200,
borderRadius: 100,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 24,
fontWeight: 'bold',
},
});
How it works
This is a standard React Native component that renders a white circle with the text "Breathe" in the center of a black screen. This gives us the basic structure for our animation.
Step 2: Creating the Breathing Animation
Now for the core of our app: the breathing animation. We'll use Reanimated's hooks to create a smooth, looping animation.
What we're doing
We'll use useSharedValue to create an animatable value that will control the scale of the circle. Then, we'll use useAnimatedStyle to apply this animated value to the circle's style. Finally, we'll use withRepeat and withSequence to create a continuous breathing cycle.
Implementation
// App.js
import React, { useEffect } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withRepeat,
withSequence,
} from 'react-native-reanimated';
export default function App() {
const scale = useSharedValue(1);
useEffect(() => {
scale.value = withRepeat(
withSequence(
withTiming(1.5, { duration: 4000 }),
withTiming(1, { duration: 4000 })
),
-1,
true
);
}, []);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
};
});
return (
<View style={styles.container}>
<Animated.View style={[styles.circle, animatedStyle]}>
<Text style={styles.text}>Breathe</Text>
</Animated.View>
</View>
);
}
// Styles remain the same
How it works
useSharedValue(1): We initialize a shared valuescalewith a value of 1. Shared values are animatable and can be accessed and modified from both the UI and JavaScript threads.useEffect: We use auseEffecthook to start the animation when the component mounts.withRepeat(...): This function creates a looping animation. We set the second argument to-1to make it repeat indefinitely, and the third argument totrueto make it reverse direction on each repeat (boomerang effect).withSequence(...): We define the animation sequence. First, the circle scales up to 1.5 over 4 seconds (withTiming(1.5, { duration: 4000 })), then it scales back down to 1 over 4 seconds (withTiming(1, { duration: 4000 })).useAnimatedStyle(...): This hook creates an animated style object. It takes a worklet (a function that can run on the UI thread) that returns a style object.transform: [{ scale: scale.value }]: We link thescaleshared value to thetransformproperty of the circle's style.Animated.View: We wrap our circleViewin anAnimated.Viewto make it animatable.
Step 3: Synchronizing Instructional Text
A visual cue is great, but instructional text makes the exercise much clearer. Let's add text that changes in sync with our animation.
What we're doing
We'll create a new shared value to hold the current instruction text. We'll then use runOnJS to update this text from the UI thread, keeping it in sync with the breathing animation.
Implementation
// App.js
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withRepeat,
withSequence,
runOnJS,
} from 'react-native-reanimated';
export default function App() {
const scale = useSharedValue(1);
const [instruction, setInstruction] = useState('Breathe');
const updateInstruction = (text) => {
setInstruction(text);
};
useEffect(() => {
scale.value = withRepeat(
withSequence(
withTiming(1.5, { duration: 4000 }, () => {
runOnJS(updateInstruction)('Hold');
}),
withTiming(1, { duration: 4000 }, () => {
runOnJS(updateInstruction)('Breathe');
})
),
-1,
true
);
}, []);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
};
});
return (
<View style={styles.container}>
<Animated.View style={[styles.circle, animatedStyle]}>
<Text style={styles.text}>{instruction}</Text>
</Animated.View>
</View>
);
}
// Styles remain the same
How it works
useState: We use a state variableinstructionto hold the text that will be displayed.runOnJS:Reanimatedanimations run on the UI thread, which cannot directly update React state.runOnJSallows us to call a JavaScript function (in this case,updateInstruction) from the UI thread.- Animation Callbacks:
withTimingaccepts an optional callback function that is executed when the animation completes. We use this to callrunOnJS(updateInstruction)with the next instruction at the appropriate time in the animation sequence.
Putting It All Together
Here is the complete code for our mindful breathing app:
// App.js
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View, StatusBar } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withRepeat,
withSequence,
runOnJS,
} from 'react-native-reanimated';
export default function App() {
const scale = useSharedValue(1);
const [instruction, setInstruction] = useState('Inhale');
const updateInstruction = (text) => {
'worklet';
runOnJS(setInstruction)(text);
};
useEffect(() => {
scale.value = withRepeat(
withSequence(
withTiming(1.5, { duration: 4000 }, () => {
updateInstruction('Hold');
}),
withTiming(1.5, { duration: 2000 }, () => {
updateInstruction('Exhale');
}),
withTiming(1, { duration: 4000 }, () => {
updateInstruction('Hold');
}),
withTiming(1, { duration: 2000 }, () => {
updateInstruction('Inhale');
})
),
-1,
false
);
}, []);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
};
});
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<Animated.View style={[styles.circle, animatedStyle]}>
<Text style={styles.text}>{instruction}</Text>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
alignItems: 'center',
justifyContent: 'center',
},
circle: {
width: 200,
height: 200,
borderRadius: 100,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 24,
fontWeight: 'bold',
},
});
Conclusion
We've successfully built a mindful breathing exercise app with a smooth, calming animation using React Native and Reanimated. We've learned how to use Reanimated's core hooks to create complex, looping animations and how to synchronize them with state updates on the JavaScript thread.
This is just the beginning. You can expand on this project by:
- Allowing the user to set the duration of the breathing exercise.
- Adding sound effects or haptic feedback.
- Creating different breathing patterns (e.g., box breathing).
Reanimated is an incredibly powerful library that can take your React Native app's user experience to the next level. I encourage you to explore its documentation and experiment with different animations.
Resources
- React Native Reanimated Documentation: https://docs.swmansion.com/react-native-reanimated/
- React Native Gesture Handler Documentation: https://docs.swmansion.com/react-native-gesture-handler/docs/