Technology /
Tools
Waarom kiezen voor React Native bij mobiele app-ontwikkeling?
:focal(1311x267:1312x268))
Cross-Platform Ontwikkeling
React Native stelt ontwikkelaars in staat om één codebasis te schrijven voor zowel iOS- als Android-platforms, wat de ontwikkelingstijd en -inspanning aanzienlijk vermindert.
Deze cross-platform compatibiliteit maakt het overbodig om aparte native apps voor elk besturingssysteem te bouwen en te onderhouden, waardoor het een ideale keuze is voor bedrijven die kosten willen besparen en een breder publiek willen bereiken.
In combinatie met Expo wordt het installatieproces nog eenvoudiger. Expo biedt een beheerde workflow die de noodzaak elimineert om native build-omgevingen te configureren—geen Xcode of Android Studio meer nodig om aan de slag te gaan.
Native-Achtige Prestaties
In tegenstelling tot sommige andere frameworks die afhankelijk zijn van webviews, gebruikt React Native een bridge om te communiceren tussen JavaScript en native code. Hierdoor kunnen componenten worden gerenderd met native API's, wat zorgt voor een native-achtige prestatie en gebruikerservaring.
Animaties, gebaren en overgangen verlopen soepel, waardoor de app nauwelijks te onderscheiden is van apps die zijn ontwikkeld met native talen zoals Swift of Kotlin.
Versnelde app-ontwikkeling met Expo-tools
React Native biedt hot reloading en live reloading, waardoor ontwikkelaars wijzigingen direct kunnen zien. Expo tilt dit naar een hoger niveau met de Expo Go-app.
Direct testen op fysieke apparaten: Met de Expo Go-app kunnen ontwikkelaars hun apps meteen op een iOS- of Android-apparaat bekijken, zonder eerst native builds te maken. Door simpelweg een QR-code te scannen, start de app direct op het apparaat.
Efficiënter werken met animaties: Bij het finetunen van animaties en overgangen is het enorm handig om wijzigingen direct op een echt apparaat te kunnen testen. Dit verkort de debugcyclus en versnelt de ontwikkeling.
Krachtige ingebouwde functies van Expo
Expo biedt een reeks ingebouwde modules die veelvoorkomende taken in mobiele ontwikkeling vereenvoudigen, zonder dat extra native code of configuratie nodig is. Dit betekent dat je meer kunt bereiken met een gestroomlijnde workflow.
Voorbeelden van Expo’s ingebouwde mogelijkheden:
Camera en media – Gebruik de expo-camera bibliotheek om toegang te krijgen tot de camera voor functies zoals beeldopname en barcodescanning.
Apparaatsensoren – Integreer eenvoudig versnellingsmeters, gyroscopen en andere sensoren.
Pushmeldingen – Met de Expo Notifications API kun je pushmeldingen verzenden en ontvangen zonder native code te schrijven.
Over-the-Air (OTA) updates – Een van de meest krachtige functies van Expo: hiermee kun je wijzigingen direct doorvoeren zonder dat gebruikers een nieuwe versie uit de App Store of Google Play hoeven te downloaden.
Hoe werken OTA-updates?
Met Expo kun je je app’s JavaScript-code en assets bijwerken en deze updates direct naar gebruikers sturen. Dit elimineert de traditionele updatecyclus waarin gebruikers handmatig updates via de appstores moeten downloaden.
Voordelen van OTA-updates:
Snellere uitrol – Breng bugfixes, nieuwe functies of contentupdates direct uit.
Verbeterde gebruikerservaring – Zorg ervoor dat gebruikers altijd de nieuwste versie hebben, zonder dat zij updates hoeven te installeren.
Geen App Store-vertragingen – Vermijd lange goedkeuringsprocessen voor niet-native wijzigingen.
Praktisch voorbeeld
Stel je voor dat je na de release een bug ontdekt. In plaats van dagen te wachten op goedkeuring van de appstores, kun je de bug direct oplossen en de update onmiddellijk naar alle gebruikers sturen. Dit is vooral handig voor contentwijzigingen, UI-aanpassingen en andere niet-native updates.
Cost-Effective and Scalable
React Native is already a cost-effective solution, allowing teams to maintain a single codebase for both iOS and Android. Expo further enhances this by reducing the time spent on native configurations, making it ideal for startups and smaller teams.
Additionally, Expo’s EAS Build (Expo Application Services) simplifies building and deploying apps. Whether you need to generate an APK for Android or a build for iOS, EAS handles it all with minimal configuration, saving you time and effort.
Community and Ecosystem
React Native and Expo benefit from active, thriving developer communities, which translate to faster problem-solving and continuous improvements.
This means quicker resolutions to any issues and access to countless pre-built libraries, saving time and development costs.
With Expo's extensive documentation and tools, many workflows are streamlined, eliminating the need to write everything from scratch. This vibrant ecosystem ensures your app stays up-to-date and benefits from the latest advancements in mobile development.
Building High-Performance Animations with React Native Reanimated
Animations play a crucial role in delivering an engaging user experience. React Native supports animations natively, and when combined with libraries like react-native-reanimated, the possibilities are endless.
Swipe-to-Delete Animation Example
Here’s a practical example: a shopping list demo app with a swipe-to-delete gesture. This demonstrates how React Native Reanimated and Gesture Handler work together to create seamless animations.
Key Features
Native-Thread Animations: Animations run on the native thread, ensuring lag-free performance.
Interactive Gestures: Gesture detection is handled smoothly, allowing for responsive swipe actions.
Declarative APIs: Simplify the creation of animations and ensure maintainability.
Code Walkthrough
Main List Component
The list is built using the FlashList component, with each item rendered using a ListItem component:
import React, { useCallback, useRef } from "react";
import { useState } from "react";
import { RefreshControl, LayoutAnimation } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { FlashList } from "@shopify/flash-list";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import ListItem from "@/components/ListItem";
interface ShoppingItem {
id: number;
name: string;
quantity: number;
category: string;
}
const sampleItems: ShoppingItem[] = [
{ id: 1, name: "Milk", quantity: 2, category: "Dairy" },
{ id: 2, name: "Bread", quantity: 1, category: "Bakery" },
{ id: 3, name: "Eggs", quantity: 12, category: "Dairy" },
{ id: 4, name: "Bananas", quantity: 6, category: "Produce" },
{ id: 5, name: "Chicken Breast", quantity: 1, category: "Meat" },
{ id: 6, name: "Tomatoes", quantity: 4, category: "Produce" },
{ id: 7, name: "Pasta", quantity: 2, category: "Pantry" },
{ id: 8, name: "Cheese", quantity: 1, category: "Dairy" },
{ id: 9, name: "Rice", quantity: 1, category: "Pantry" },
{ id: 10, name: "Apples", quantity: 5, category: "Produce" },
];
export default function HomeScreen() {
const insets = useSafeAreaInsets();
const [refreshing, setRefreshing] = useState<boolean>(false);
const list = useRef<FlashList<number> | null>(null);
const [items, setItems] = useState<ShoppingItem[]>(sampleItems);
const handleDelete = useCallback(
(index: number) => {
setTimeout(() => {
setItems((currentItems) => currentItems.filter((_, i) => i !== index));
// This must be called before `LayoutAnimation.configureNext` in order for the animation to run properly.
list?.current?.prepareForLayoutAnimationRender();
// After removing the item, we can start the animation.
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
list?.current?.recordInteraction();
}, 128);
},
[list?.current]
);
const renderItem = useCallback(
({ item, index }: { item: any; index: number }) => {
return <ListItem onDelete={handleDelete} item={item} index={index} />;
},
[handleDelete]
);
return (
<GestureHandlerRootView>
<FlashList
removeClippedSubviews
ref={list}
extraData={items}
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
keyExtractor={(item, index) => index.toString()}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={() => {
setRefreshing(true);
setTimeout(() => {
setItems(sampleItems);
setRefreshing(false);
}, 1000);
}}
/>
}
estimatedItemSize={80}
contentContainerStyle={{
paddingTop: insets.top,
paddingHorizontal: 24,
backgroundColor: "#ffffff",
}}
data={items}
renderItem={renderItem}
/>
</GestureHandlerRootView>
);
}
List Item with Swipe-to-Delete Gesture
The ListItem component implements the swipe gesture using react-native-gesture-handler and animates the deletion with react-native-reanimated:
import { AntDesign } from "@expo/vector-icons";
import React from "react";
import { useEffect } from "react";
import { Alert, Pressable, useWindowDimensions, Text } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
interface ShoppingItem {
id: number;
name: string;
quantity: number;
category: string;
}
// Memoized ListItem to optimize performance.
const ListItem = React.memo(
({
index,
item,
onDelete,
}: {
index: number;
item: ShoppingItem;
onDelete: (index: number, showSuccessAlert?: boolean) => void;
}) => {
const position = useSharedValue(0);
const actionButtonWidth = useSharedValue(0);
const { width } = useWindowDimensions();
const THRESHOLDX = -width * 0.32;
// Gesture for detecting swiping actions.
const panGesture = Gesture.Pan()
.onStart((e) => {
if (e.translationX <= 0) {
position.value = e.translationX;
if (e.translationX <= THRESHOLDX) {
actionButtonWidth.value = THRESHOLDX;
} else {
actionButtonWidth.value = e.translationX;
}
}
})
.onUpdate((e) => {
if (e.translationX <= 0) {
position.value = e.translationX;
if (e.translationX <= THRESHOLDX) {
actionButtonWidth.value = THRESHOLDX;
} else {
actionButtonWidth.value = e.translationX;
}
}
})
.onEnd((e) => {
if (e.translationX < THRESHOLDX) {
position.value = withTiming(THRESHOLDX - 8);
} else {
actionButtonWidth.value = withTiming(0);
position.value = withTiming(0);
}
})
.activeOffsetX([-8, 8]);
// Style for the swiping animation.
const panAnimation = useAnimatedStyle(() => {
return {
transform: [{ translateX: position.value }],
};
}, []);
// Style for the delete button animation.
const deleteAnimation = useAnimatedStyle(() => {
return {
width: Math.abs(actionButtonWidth.value),
};
}, []);
// Handles item deletion after confirmation.
const handleDelete = () => {
Alert.alert(
"Delete Item",
`Are you sure you want to delete item ${item?.name}?`,
[
{
text: "Cancel",
style: "cancel",
onPress: () => {
// Reset the item position when cancelled
actionButtonWidth.value = withTiming(0);
position.value = withTiming(0);
},
},
{
text: "Delete",
style: "destructive",
onPress: () => {
actionButtonWidth.value = withTiming(0, { duration: 300 });
position.value = withTiming(
-width,
{
duration: 300,
},
() => {
runOnJS(onDelete)(index);
}
);
},
},
]
);
};
useEffect(() => {
// Reset value when id changes (view was recycled for another item)
position.value = 0;
actionButtonWidth.value = 0;
}, [item.id, position]);
return (
<GestureDetector gesture={panGesture}>
<Animated.View
style={{
flexDirection: "row",
marginVertical: 8,
width: "100%",
alignItems: "center",
position: "relative",
}}
>
{/* Swipable item */}
<Animated.View
style={[
panAnimation,
{
width: "100%",
borderRadius: 8,
paddingHorizontal: 16,
paddingVertical: 24,
backgroundColor: "#ffffff",
shadowColor: "#000000",
shadowOffset: {
width: 0,
height: 0,
},
shadowOpacity: 0.2,
shadowRadius: 16,
elevation: 12,
alignItems: "center",
justifyContent: "center",
},
]}
>
<Text>{item?.name}</Text>
</Animated.View>
{/* Delete button */}
<Animated.View
style={[
deleteAnimation,
{
height: "100%",
backgroundColor: "#e63946",
borderRadius: 8,
alignItems: "center",
position: "absolute",
right: 0,
marginLeft: 8,
justifyContent: "center",
transformOrigin: "right",
},
]}
>
<Pressable
onPress={handleDelete}
style={({ pressed }) => [
{
width: "100%",
height: "100%",
alignItems: "center",
justifyContent: "center",
opacity: pressed ? 0.24 : 1,
},
]}
>
<AntDesign name="delete" size={24} color="#f1faee" />
</Pressable>
</Animated.View>
</Animated.View>
</GestureDetector>
);
}
);
export default ListItem;
Live Demonstration
Here’s a video showing the swipe-to-delete animation in action on iOS and Android :