Technology /

Tools

Waarom kiezen voor React Native bij mobiele app-ontwikkeling?

On 23 / 01 / 2025 / by Georgi Ivanov

React native
Het kiezen van het juiste framework voor je mobiele app is geen gemakkelijke taak. React Native, in combinatie met Expo, biedt een krachtige oplossing die een balans vindt tussen prestaties, efficiëntie en schaalbaarheid. React Native springt eruit als een toonaangevende keuze voor het bouwen van hoogwaardige apps voor zowel iOS als Android, en Expo gaat nog een stap verder door workflows te vereenvoudigen en robuuste tools te bieden. Laten we onderzoeken waarom deze combinatie een game-changer is voor mobiele ontwikkeling—plus een praktijkvoorbeeld dat de animatiemogelijkheden van React Native laat zien.

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 :