I’m excited to share my new React Native package: react-native-animated-reactions. This package makes it effortless to add animated reaction bars (think: like, love, laugh, wow, sad, and angry) to your React Native apps. In this post, I’ll walk you through what the package does, how it works under the hood, and how you can integrate it into your own projects.
What Is React Native Animated Reactions?
At its core, the package provides a customizable ReactionBar component that displays a row of animated reaction icons. It leverages react-native-reanimated to deliver smooth, natural animations that look great on both iOS and Android devices. The component is built with performance and simplicity in mind, so you can add interactive reactions to your user interface with just a few lines of code.
In case you don't have time to go through this post, you can go straight to GitHub repository Available here
Please don't forget to give it a starIf you want to test it on your phone or play with it, you can use this expo snack
Lets dive Into the Code
Create index.tsx file to host
ReactionBar
component code
Defining Types
export type Reaction = {
id: string;
iconUrl: string;
};
type ReactionBarProps = {
onDismiss: () => void;
onReactionSelect: (reaction: Reaction) => void;
};
export type ReactionBarRef = {
showReactions: () => void;
dismissReactions: () => void;
};
- Reaction: A simple type defining each reaction with an ID and URL.
- ReactionBarProps: Props expected by the component include callbacks for when the bar is dismissed or when a reaction is selected.
- ReactionBarRef: The imperative methods that the parent can call to control the component.
Component Definition with forwardRef.
const ReactionBar = forwardRef<ReactionBarRef, ReactionBarProps>(
({ onDismiss, onReactionSelect }, ref) => {
// ...
}
);
- forwardRef is used so that parent components can directly call methods defined inside ReactionBar.
Setting Up Animation Shared Values
const scale = useSharedValue(0);
const bounce = useSharedValue(1);
- scale: Controls the overall scale (and opacity, as mapped later) of the reaction bar.
- bounce: Controls the bounce animation effect when a reaction is selected.
Memoizing the Reaction Data
const iconUrls: string[] = [
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Flike.png?alt=media&token=42e892f7-f7a0-4f6b-82cd-2ff8844d4483',
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Flove.png?alt=media&token=f762c537-f639-4925-9c1a-7400636eea68',
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Fhaha.png?alt=media&token=0dc5cdf9-84cd-4ed1-9a16-7ed1200131a7',
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Fwow.png?alt=media&token=9637475f-92d8-4910-9666-ec049e9de65a',
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Fsad.png?alt=media&token=d1990a7e-5bed-4a0e-b9f8-8837503abf4c',
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Fangry.png?alt=media&token=8d0c6bf4-fcc0-4283-821f-d51b77064ea3',
];
const reactions = useMemo<Reaction[]>(
() => [
{ id: 'like', iconUrl: iconUrls[0] },
{ id: 'love', iconUrl: iconUrls[1] },
{ id: 'haha', iconUrl: iconUrls[2] },
{ id: 'wow', iconUrl: iconUrls[3] },
{ id: 'sad', iconUrl: iconUrls[4] },
{ id: 'angry', iconUrl: iconUrls[5] },
],
[]
);
- useMemo ensures the reaction list is only computed once unless dependencies change.
- I decided to use remote versions of icon pictures to make it easy to store the same icons in the database if you want otherwise you can use local icons
Animated Styles
const reactionBarStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: scale.value,
}));
const bounceStyle = useAnimatedStyle(() => ({
transform: [{ scale: bounce.value }],
}));
- reactionBarStyle: Uses the scale value to animate both the scale and opacity.
- bounceStyle: Applies a bounce effect on each reaction icon.
Animation Functions
Show Reactions
const showReactions = useCallback(() => {
scale.value = withSpring(1, { damping: 10, stiffness: 100 });
}, [scale]);
- Animates the scale from 0 to 1 with a spring effect, making the reaction bar appear. Dismiss Reactions
const dismissReactions = useCallback(() => {
scale.value = withTiming(0, { duration: 200 });
}, [scale]);
- Animates the scale from 1 back to 0 using a timing animation, hiding the bar.
Exposing Imperative Methods
useImperativeHandle(ref, () => ({
showReactions,
dismissReactions,
}));
- Exposes the showReactions and dismissReactions functions to the parent component via the ref.
Handling Reaction Selection
const handleReactionSelect = useCallback(
(id: string) => {
const selectedReaction = reactions.find((reaction) => reaction.id === id);
if (selectedReaction) {
onReactionSelect(selectedReaction);
}
// Trigger bounce effect
bounce.value = 1.5;
bounce.value = withSpring(1, { damping: 5, stiffness: 150 });
dismissReactions();
},
[bounce, dismissReactions, onReactionSelect, reactions]
);
- Finds the selected reaction and calls onReactionSelect.
- Triggers a bounce effect by temporarily increasing bounce.value.
- Calls dismissReactions to hide the reaction bar after selection.
- Using useCallback with the proper dependency array ensures that this function remains stable across renders unless one of its dependencies changes, which is essential for performance optimization in React applications.
Listening to Animation Changes
useAnimatedReaction(
() => scale.value === 0,
(isDismissed) => {
if (isDismissed) {
runOnJS(onDismiss)();
}
}
);
- Monitors scale.value and when it becomes 0 (bar is hidden), it calls onDismiss using runOnJS.
Component styles
const styles = StyleSheet.create({
reactionBar: {
flexDirection: 'row',
backgroundColor: '#FFFFFF',
borderRadius: 30,
padding: 10,
position: 'absolute',
bottom: 100,
alignSelf: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 5,
},
reactionIconWrapper: {
marginHorizontal: 9,
},
reactionIcon: {
width: 32,
height: 32,
},
- Here, we define the styles of the Reaction component.
Rendering the Component
return (
<Animated.View style={[styles.reactionBar, reactionBarStyle]}>
{reactions.map((reaction) => (
<TouchableOpacity
key={reaction.id}
onPress={() => handleReactionSelect(reaction.id)}
style={styles.reactionIconWrapper}
>
<Animated.Image
source={{ uri: reaction.iconUrl }}
style={[styles.reactionIcon, bounceStyle]}
/>
</TouchableOpacity>
))}
</Animated.View>
);
- Renders an animated view with the reaction bar style.
- Maps over reactions to create a TouchableOpacity for each reaction, wrapping an animated image.
- Tapping an icon triggers handleReactionSelect.
Full code
import { useImperativeHandle, forwardRef, useCallback, useMemo } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
useAnimatedReaction,
runOnJS,
} from 'react-native-reanimated';
const iconUrls: string[] = [
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Flike.png?alt=media&token=42e892f7-f7a0-4f6b-82cd-2ff8844d4483',
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Flove.png?alt=media&token=f762c537-f639-4925-9c1a-7400636eea68',
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Fhaha.png?alt=media&token=0dc5cdf9-84cd-4ed1-9a16-7ed1200131a7',
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Fwow.png?alt=media&token=9637475f-92d8-4910-9666-ec049e9de65a',
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Fsad.png?alt=media&token=d1990a7e-5bed-4a0e-b9f8-8837503abf4c',
'https://firebasestorage.googleapis.com/v0/b/firecast-6a07f.appspot.com/o/icons%2Fangry.png?alt=media&token=8d0c6bf4-fcc0-4283-821f-d51b77064ea3',
];
export type Reaction = {
id: string;
iconUrl: string;
};
type ReactionBarProps = {
onDismiss: () => void;
onReactionSelect: (reaction: Reaction) => void; // Callback for selected reaction
};
export type ReactionBarRef = {
showReactions: () => void;
dismissReactions: () => void;
};
const ReactionBar = forwardRef<ReactionBarRef, ReactionBarProps>(
({ onDismiss, onReactionSelect }, ref) => {
// Reanimated shared values for animations
const scale = useSharedValue(0);
const bounce = useSharedValue(1);
const reactions = useMemo<Reaction[]>(
() => [
{ id: 'like', iconUrl: iconUrls[0]! },
{ id: 'love', iconUrl: iconUrls[1]! },
{ id: 'haha', iconUrl: iconUrls[2]! },
{ id: 'wow', iconUrl: iconUrls[3]! },
{ id: 'sad', iconUrl: iconUrls[4]! },
{ id: 'angry', iconUrl: iconUrls[5]! },
],
[]
);
// Animated styles
const reactionBarStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: scale.value,
}));
const bounceStyle = useAnimatedStyle(() => ({
transform: [{ scale: bounce.value }],
}));
// Show reactions
const showReactions = useCallback(() => {
scale.value = withSpring(1, { damping: 10, stiffness: 100 });
}, [scale]);
// Dismiss reactions
const dismissReactions = useCallback(() => {
scale.value = withTiming(0, { duration: 200 });
}, [scale]);
// Handle reaction selection
const handleReactionSelect = useCallback(
(id: string) => {
const selectedReaction = reactions.find(
(reaction) => reaction.id === id
);
if (selectedReaction) {
onReactionSelect(selectedReaction);
}
// Trigger bounce effect
bounce.value = 1.5;
bounce.value = withSpring(1, { damping: 5, stiffness: 150 });
dismissReactions();
},
[bounce, dismissReactions, onReactionSelect, reactions]
);
useAnimatedReaction(
() => scale.value === 0,
(isDismissed) => {
if (isDismissed) {
runOnJS(onDismiss)();
}
}
);
// Expose methods to parent component
useImperativeHandle(ref, () => ({
showReactions,
dismissReactions,
}));
return (
<Animated.View style={[styles.reactionBar, reactionBarStyle]}>
{reactions.map((reaction) => (
<TouchableOpacity
key={reaction.id}
onPress={() => handleReactionSelect(reaction.id)}
style={styles.reactionIconWrapper}
>
<Animated.Image
source={{ uri: reaction.iconUrl }}
style={[styles.reactionIcon, bounceStyle]}
/>
</TouchableOpacity>
))}
</Animated.View>
);
}
);
const styles = StyleSheet.create({
reactionBar: {
flexDirection: 'row',
backgroundColor: '#FFFFFF',
borderRadius: 30,
padding: 10,
position: 'absolute',
bottom: 100,
alignSelf: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 5,
},
reactionIconWrapper: {
marginHorizontal: 9,
},
reactionIcon: {
width: 32,
height: 32,
},
});
export default ReactionBar;
Component usage in real world
Create app.tsx to use our ReactionBar component
import { useRef, useState } from 'react';
import { Image, Pressable,StyleSheet, Text, TouchableOpacity, View,} from 'react-native';
import ReactionBar, { type ReactionBarRef,type Reaction} from '.';
const App = () => {
// Here we use ReactionBarRef and Reaction types exported in index.tsx
const reactionBarRef = useRef<ReactionBarRef>(null);
const [selectedReaction, setSelectedReaction] = useState<Reaction | null>(
null
);
// Here, we get access to the selected reaction, you can save it to database or anything else
const handleReactionSelect = (reaction: Reaction) => {
console.log('Selected Reaction:', reaction);
setSelectedReaction(reaction);
};
const onDismiss = () => {
console.log('Reaction bar dismissed');
};
const showReaction = () => {
// Here we access showReactions method exposed by ReactionBar through the reference
reactionBarRef.current?.showReactions();
};
const dismissReaction = () => {
// Here, we access the dismissReactions method exposed by ReactionBar through the reference.
reactionBarRef.current?.dismissReactions();
};
return (
<Pressable style={styles.container} onPress={dismissReaction}>
{// If there is selected item show it on screen}
{selectedReaction && (
<View style={styles.selectedItem}>
<Text style={styles.selectedReactionText}>
Selected Reaction: {selectedReaction.id}
</Text>
<Image
source={{ uri: selectedReaction.iconUrl }}
style={styles.selectedIcon}
/>
</View>
)}
<TouchableOpacity onPress={showReaction} style={styles.triggerButton}>
<Text style={styles.triggerButtonText}>Show Reactions</Text>
</TouchableOpacity>
<ReactionBar
ref={reactionBarRef}
onReactionSelect={handleReactionSelect}
onDismiss={onDismiss}
/>
</Pressable>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
triggerButton: {
padding: 10,
backgroundColor: '#007BFF',
borderRadius: 5,
},
triggerButtonText: {
color: '#FFFFFF',
fontWeight: 'bold',
},
selectedReactionText: {
fontSize: 16,
color: '#333',
},
selectedItem: {
marginBottom: 20,
flexDirection: 'row',
gap: 30,
},
selectedIcon: {
width: 32,
height: 32,
},
});
export default App;
In case you want to use gestures like long press, as is the case with Facebook and WhatsApp
We need to use react-native-gesture-handler
, you can follow the setup and installation here
import { GestureHandlerRootView, LongPressGestureHandler, State } from 'react-native-gesture-handler';
mport { useRef, useState } from 'react';
import { Text,TouchableOpacity} from 'react-native';
import ReactionBar, { type ReactionBarRef, type Reaction} from '.';
const App = ()=>{
const reactionBarRef = useRef<ReactionBarRef>(null);
const [selectedReaction, setSelectedReaction] = useState<Reaction | null>(null);
const handleReactionSelect = (reaction: Reaction) => {
console.log('Selected Reaction:', reaction);
setSelectedReaction(reaction);
};
const onDismiss = () => {
console.log('Reaction bar dismissed');
};
return (
<GestureHandlerRootView>
<LongPressGestureHandler
onHandlerStateChange={({ nativeEvent }) => {
if (nativeEvent.state === State.ACTIVE) {
reactionBarRef.current?.showReactions();
}
}}
minDurationMs={500}
>
<TouchableOpacity>
<Text>Press & Hold</Text>
</TouchableOpacity>
</LongPressGestureHandler>
<ReactionBar
ref={reactionBarRef}
onReactionSelect={handleReactionSelect}
onDismiss={onDismiss}
/>
</GestureHandlerRootView>
)
}
Summary
- Imperative Control: The component uses
forwardRef
anduseImperativeHandle
to exposeshowReactions
anddismissReactions
for external control. - Real-Time Animations: Shared values (
scale
andbounce
) and animated styles (useAnimatedStyle
) provide smooth transitions. - User Interaction: Tapping an icon triggers a bounce animation, dismisses the bar, and calls the provided callback with the selected reaction.
- Reactive Updates:
useAnimatedReaction
monitors changes inscale.value
and callsonDismiss
when the bar is hidden.
Refferences
- React Native Reanimated Documentation
- React Documentation: forwardRef
- React Documentation: useImperativeHandle
Final Thoughts
The ReactionBar component is designed to bring fun, interactive, and smooth animated reactions to your app with minimal hassle. With an easy-to-use API, sensible defaults, and the flexibility to extend and customize, it’s a great tool for any React Native developer looking to enhance user interactions.
I hope this breakdown of this component's source code and features helps you understand its inner workings and inspires you to try it out in your next project. If you have any questions, suggestions, or ideas for improvement, feel free to comment or reach out on GitHub. The more optimized and maintained version is available on npm
I am available for consultation and Gigs for the advanced apps React, React-native, Nextjs, Nodejs, Firebase and more
Top comments (0)