Lesson 1 · React Native

Meet React Native (Deep Dive)

Flutter is a wonderful way to build apps, but it is not the only one. The other giant in cross-platform development is React Native, created by Facebook (Meta) and used to build apps like Instagram, Discord and parts of Facebook itself. It is worth understanding, because it takes a genuinely different approach — and because it uses JavaScript, the world's most widely-known programming language.

A Different Philosophy from Flutter

Recall that Flutter is like a painter: it draws every pixel of every button itself, so your app looks identical on all devices. React Native works differently — it is more like a manager giving instructions. You write JavaScript, and React Native asks the phone's own operating system to create real, native iOS and Android components on your behalf.

Painter vs. Manager: Flutter paints its own button onto a blank canvas, giving total control over the look. React Native tells iOS "please make one of your standard buttons here," so the app uses the phone's genuine native pieces. Both are valid strategies with real trade-offs — neither is simply "better."

Under the Hood: Threads & Architecture

To truly master React Native, you must understand its threading model. A React Native app runs on three main threads:

The Bridge vs. The New Architecture (JSI)

Historically, the JS thread communicated with the UI thread via a Bridge — converting instructions to JSON strings, sending them across asynchronously, and translating them back. While effective, this serialization caused performance bottlenecks during heavy animations or rapid scrolling.

Today, React Native fundamentally changed this with the New Architecture, built on three pillars:

Alongside this, React Native now defaults to using Hermes, a lightweight JavaScript engine built by Meta specifically for running React Native on mobile. It pre-compiles your JS code into bytecode, meaning your app launches significantly faster and uses less memory.

The concepts transfer: Everything you learned in Dart and Flutter — variables, logic, functions, state, UI as building blocks — exists in React Native too, just with different names and syntax. Learning a second framework is far easier than the first, because the ideas are already yours.
Animated Example

JavaScript drives real native components

Your JS
code
JSI / Bridge
translates
Real native
iOS/Android
  1. You write your app in JavaScript — the world's most-known language.
  2. The JSI / Bridge translates your instructions synchronously or asynchronously…
  3. …and asks the phone to build real native buttons and views. Unlike Flutter, it uses the platform's own pieces.

Key Takeaways

  • React Native is the other big cross-platform tool, made by Meta, using JavaScript.
  • Unlike Flutter (which paints pixels), it asks the phone to create real native components.
  • It operates on three main threads: UI, JS, and Background layout (Yoga).
  • The New Architecture (JSI, Fabric, TurboModules) removes the asynchronous bridge, offering near-native performance.

Your Turn: Practice

List three apps you have heard are built with React Native (search if needed). Then describe in your own words the difference between the old Bridge architecture and the New Architecture (JSI).

Lesson 2 · React Native

Advanced Components & JSX

Where Flutter has "widgets," React Native has components. The idea is beautifully similar: your interface is built from small, reusable pieces that you combine into bigger ones. Let's dive deeper into component patterns.

A Component is Just a Function

In modern React Native, a component is a JavaScript function that returns some UI. Here is a tiny one that displays a greeting:

function Greeting() { return ( <Text>Hello, World!</Text> ); }

That component can now be reused anywhere, like a custom building block: <Greeting />.

JSX Deep Dive & Conditional Rendering

JSX lets you describe your UI using HTML-like tags inside JavaScript. Because it is JavaScript, you can seamlessly embed logic using curly braces {}.

One of the most powerful features of JSX is Conditional Rendering. Instead of writing complex if-else statements, you can use the logical AND operator (&&) or the ternary operator (? :):

function UserProfile({ isLoggedIn, username }) { return ( <View> {isLoggedIn ? ( <Text>Welcome back, {username}!</Text> ) : ( <Button title="Please Log In" /> )} {/* Logical AND: Only render badge if user is Admin */} {username === 'Admin' && <Badge title="Superuser" />} </View> ); }

The Golden Rule of JSX & Fragments

A component in React Native must return exactly one top-level element. If you don't want to use a wrapper <View> that affects layout (like Flutter's Column or Row), you use a Fragment (<>...</>):

return ( <> <Text>Item 1</Text> <Text>Item 2</Text> </> );

Props Destructuring & Default Values

Props (properties) pass data down to components. Modern JS developers use destructuring and can even set default values directly in the function signature:

function Avatar({ url, size = 50, showBorder = false }) { return ( <Image source={{ uri: url }} style={{ width: size, height: size, borderWidth: showBorder ? 2 : 0 }} /> ); }

Composition with children

React Native uses a special prop called children for component composition. It allows you to create wrapper components (like a Card) without knowing what will go inside them beforehand:

function Card({ children, elevation = 2 }) { return ( <View style={{ padding: 20, borderRadius: 10, backgroundColor: 'white', shadowOpacity: elevation * 0.1 }}> {children} </View> ); } // Usage: <Card elevation={5}> <Text>Title inside card</Text> <Button title="Action" /> </Card>

Custom Hooks: Reusing Stateful Logic

When you have logic that you want to share across multiple components (like tracking the device's online status or a countdown timer), you don't copy-paste the state. Instead, you extract it into a Custom Hook.

function useOnlineStatus() { const [isOnline, setIsOnline] = useState(true); useEffect(() => { // Mock listener setup const sub = Network.addListener(status => setIsOnline(status)); return () => sub.remove(); }, []); return isOnline; } // Now any component can use it: const isOnline = useOnlineStatus();

The same idea, everywhere

Notice how this mirrors Flutter almost exactly: reusable pieces (widgets / components) that accept inputs (constructor arguments / props) to customise them. This "build your UI from small reusable pieces" philosophy is the dominant approach in modern app development.

Animated Example

Composing a UI from components

<App />
<Feed />
  1. Everything lives inside one top-level component: <App />.
  2. It renders a reusable <Navbar /> component.
  3. …and a <Feed /> component. Nest components to build any interface.

Key Takeaways

  • React Native's building blocks are components (functions that return JSX).
  • Conditional rendering with && and ? : is a powerful alternative to if/else.
  • Destructuring props with default values makes code significantly cleaner.
  • The children prop enables flexible component composition, like Flutter's `child` or `children`.

Your Turn: Practice

Write a React Native component named WarningBanner that takes two props: message and isVisible. Use conditional rendering to return a <Text> component containing the message only if isVisible is true, otherwise return null.

Lesson 3 · React Native

Advanced Core Components

React Native gives you a set of fundamental components to build any screen. Let's look at them in depth, including advanced components for handling keyboards, complex lists, and interactive elements.

View & SafeAreaView

A View is the basic building block (like Flutter's Container or a web div). Almost everything lives inside a View.

Modern phones have notches, dynamic islands, and home indicator bars. SafeAreaView is a special View that ensures your content is rendered within the safe area boundaries of an iOS device, preventing text from being hidden behind hardware elements.

Handling Keyboards: KeyboardAvoidingView

On mobile, when the user taps a text input, the virtual keyboard slides up. By default, it will cover your UI. React Native provides KeyboardAvoidingView to automatically move your inputs out of the way.

<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }} > <TextInput placeholder="Type a message..." /> </KeyboardAvoidingView>

Advanced Lists: FlatList & SectionList

For long lists, FlatList is essential because it is "lazy" — it only renders the items currently visible on the screen, recycling off-screen views. For lists with headers (like a contact list sorted by alphabet), use SectionList:

<SectionList sections={[ { title: 'A', data: ['Alice', 'Andrew'] }, { title: 'B', data: ['Bob', 'Brian'] } ]} renderItem={({ item }) => <Text>{item}</Text>} renderSectionHeader={({ section }) => ( <Text style={{ fontWeight: 'bold', backgroundColor: '#eee' }}> {section.title} </Text> )} />

Pull-to-Refresh with RefreshControl

You can easily add "pull-to-refresh" functionality to any ScrollView, FlatList, or SectionList by using the refreshControl prop:

<FlatList data={posts} refreshControl={ <RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} tintColor="#61dafb" /> } />

Interaction: Pressable vs TouchableOpacity

While TouchableOpacity is older and provides a simple fade effect, modern React Native recommends Pressable. Pressable gives you fine-grained control over various press states (pressed in, hovered, focused) allowing for complex interactions.

<Pressable onPress={() => alert('Tapped!')} style={({ pressed }) => [ { backgroundColor: pressed ? '#ddd' : 'white' }, styles.button ]} > <Text>Press Me</Text> </Pressable>
Flutter (Dart)React Native (JS)
Container / SafeAreaView / SafeAreaView
ListView.builderFlatList
RefreshIndicatorRefreshControl
GestureDetector / InkWellPressable / TouchableOpacity
Animated Example

React Native's core building blocks

<View> — the container
<Pressable> — interactions
<KeyboardAvoidingView>
<SectionList> — fast grouped lists
  1. <View> is the container — like Flutter's Container or a web div.
  2. <Pressable> detects complex touch and hold gestures.
  3. <KeyboardAvoidingView> prevents the keyboard from hiding your inputs.
  4. <SectionList> renders long grouped lists with sticky headers efficiently.

Key Takeaways

  • KeyboardAvoidingView is essential for forms on mobile devices.
  • SectionList handles lists with categorized headers, while FlatList is for flat arrays.
  • RefreshControl easily adds pull-to-refresh to any scrolling component.
  • Pressable is the modern, powerful alternative to TouchableOpacity.

Your Turn: Practice

For each React Native component in this lesson, write down its Flutter equivalent from memory. If you can map all of them, you have proven the two frameworks share one mental model.

Lesson 4 · React Native

Advanced Styling & Hooks (State)

Now let us make React Native components look good and respond to the user. We will explore Flexbox layout mastery and advanced React Hooks.

Styling: StyleSheet & Absolute Positioning

While you can pass inline styles, using StyleSheet.create provides performance benefits (styles are created once, not every render) and autocompletion.

Unlike the web, React Native defaults to display: 'flex' and flexDirection: 'column'. For overlapping UI (like a floating action button), you use absolute positioning:

const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', // Align along main axis (vertical) alignItems: 'center', // Align along cross axis (horizontal) }, fab: { position: 'absolute', bottom: 20, right: 20, backgroundColor: '#61dafb', padding: 15, borderRadius: 30, } });

Advanced State: useReducer

When state logic gets complex (e.g., managing a shopping cart with ADD, REMOVE, CLEAR actions), useState can become messy. React offers useReducer, which works similarly to Redux, keeping your state transitions predictable.

function cartReducer(state, action) { switch (action.type) { case 'ADD': return { total: state.total + action.price }; case 'RESET': return { total: 0 }; default: return state; } } function Cart() { const [state, dispatch] = useReducer(cartReducer, { total: 0 }); return <Button title="Add $10" onPress={() => dispatch({ type: 'ADD', price: 10 })} />; }

Side Effects & Cleanup: useEffect

The useEffect Hook runs code automatically after the component renders. A crucial concept is the cleanup function. If your effect sets up a subscription or a timer, you must clean it up to prevent memory leaks when the component unmounts.

useEffect(() => { const intervalId = setInterval(() => { console.log('Tick'); }, 1000); // The return function acts as "cleanup" (like Flutter's dispose) return () => { clearInterval(intervalId); }; }, []); // Empty array: run once on mount, cleanup on unmount

Performance: useMemo and useCallback

In React, whenever state changes, the component re-renders. If you have a heavy calculation or pass functions to child components, they get recreated every render.
useMemo caches the result of a calculation. useCallback caches a function definition.

// Only recalculate if "items" array changes const expensiveTotal = useMemo(() => calculateTotal(items), [items]); // Only recreate this function if "userId" changes const handlePress = useCallback(() => { deleteUser(userId); }, [userId]);

Platform-Specific Code: Platform.select

Sometimes you need different behavior or styling depending on whether the app is running on iOS or Android. React Native provides the Platform API to handle this cleanly without messy if-else chains.

import { Platform, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ container: { ...Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.2, }, android: { elevation: 4, // Android uses elevation instead of shadows }, }), } });

One mental model, two toolkits

By now you should feel it clearly: components and widgets, useState and setState, Flexbox and Row/Column, useEffect return function and dispose() — these are the same handful of ideas appearing in both frameworks.

Animated Example

useReducer for complex state

0
dispatch('ADD')
dispatch('RESET')
  1. User taps ADD. An action is dispatched to the reducer function.
  2. User taps RESET. The reducer handles complex logic predictably. State changes → component re-renders.

Key Takeaways

  • Use StyleSheet.create for performant styling. React Native relies heavily on Flexbox and absolute positioning.
  • useReducer is excellent for managing complex, multi-action state.
  • Always return a cleanup function in useEffect if you start a timer, listener, or subscription.
  • Use useMemo and useCallback to optimize performance and prevent unnecessary re-renders.

Your Turn: Practice

Recall the Flutter `dispose()` method. Now describe, in words, how the same cleanup logic is implemented in React Native using useEffect.

Lesson 5 · React Native

Advanced Routing & Navigation

Real apps have many screens, and React Native — unlike Flutter — doesn't include navigation in its core. Instead you add the community-standard library React Navigation. The mental model is identical to Flutter's: screens live on a stack, and you push and pop them like a deck of cards.

Passing Parameters Deeply

When you navigate, you can pass parameters. But how do you handle complex data or ensure type safety? React Navigation allows you to pass params in the second argument of navigate, and the receiving screen reads them via the route.params object:

// 1. From Home Screen: Pass complex data navigation.navigate('UserProfile', { userId: 42, sortOrder: 'desc' }); // 2. Inside UserProfile Screen function UserProfile({ route, navigation }) { const { userId, sortOrder } = route.params; return <Text>Viewing user {userId} sorted {sortOrder}</Text>; }

Nesting Navigators

Modern apps rarely rely on just a stack. You often have a Tab Navigator at the bottom, and inside one of those tabs (e.g., "Profile"), you have a Stack Navigator so users can click into specific profile details.

Here is how a nested architecture looks:

const Stack = createNativeStackNavigator(); const Tab = createBottomTabNavigator(); function ProfileStack() { return ( <Stack.Navigator> <Stack.Screen name="ProfileMain" component={ProfileScreen} /> <Stack.Screen name="Settings" component={SettingsScreen} /> </Stack.Navigator> ); } function AppNavigator() { return ( <Tab.Navigator> <Tab.Screen name="Home" component={HomeScreen} /> {/* Nesting the Stack inside a Tab */} <Tab.Screen name="Profile" component={ProfileStack} /> </Tab.Navigator> ); }

Authentication Flows

How do you prevent logged-out users from seeing the Home screen? You conditionally render navigators based on authentication state.

function RootNavigator() { const { isLoggedIn } = useAuthContext(); return ( <NavigationContainer> {isLoggedIn ? ( <AppNavigator /> {/* Shows Home, Profile, etc. */} ) : ( <AuthNavigator /> {/* Shows Login, Signup screens */} )} </NavigationContainer> ); }

This guarantees a user cannot navigate back to a protected screen after logging out, because those screens are entirely unmounted from the tree.

Deep Linking & Universal Links

Advanced apps handle Deep Linking, allowing a web URL (e.g., myapp://profile/42 or https://myapp.com/profile/42) to open the app directly to a specific screen. React Navigation supports this natively with a configuration object.

const linking = { prefixes: ['myapp://', 'https://myapp.com'], config: { screens: { Home: 'home', ProfileStack: { screens: { ProfileMain: 'profile/:id', // Parses the ID from the URL } } } } }; // Usage: <NavigationContainer linking={linking}>
The same stack of cards. Whether you're in Flutter (Navigator.push) or React Native (navigation.navigate), the idea is one: opening a screen lays a new card on the pile; going back removes the top card.
Animated Example

Authentication Flow Routing

Login
screen
Home
Stack
isLoggedIn = true → swap navigators
  1. User is not logged in. React renders the AuthNavigator.
  2. User logs in. State changes, React instantly unmounts Auth and renders the AppNavigator. No "back" button can defeat this!

Key Takeaways

  • React Native uses React Navigation; screens live on a stack.
  • You can pass parameters seamlessly through the navigate method and read them via route.params.
  • Nesting Navigators (e.g., Stacks inside Tabs) is the standard for complex app architectures.
  • Use Conditional Rendering at the root level for bulletproof Authentication Flows.

Your Turn: Practice

Sketch the screen flow for an E-commerce app. Include an Auth flow, a Tab navigator for Home/Cart/Profile, and a Stack navigator inside the Home tab for viewing Product Details.

Lesson 6 · React Native

Data Fetching & State Management

Fetching data is straightforward with fetch, but managing that data across a large app requires robust strategies. We will look at standard data fetching and modern caching tools.

The fetch API with Hooks

To fetch data when a screen opens and display it on the UI, you combine useState (to store data, loading, and error states) and useEffect.

function UserProfile() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function loadData() { try { const res = await fetch('https://api.example.com/me'); if (!res.ok) throw new Error('Network error'); const data = await res.json(); setUser(data); } catch(err) { setError(err.message); } finally { setLoading(false); } } loadData(); }, []); if (loading) return <ActivityIndicator size="large" />; if (error) return <Text style={{color: 'red'}}>Error: {error}</Text>; return <Text>Hello, {user.name}</Text>; }

React Query (TanStack Query)

Writing the above boilerplate for every screen is exhausting. Modern React Native apps use libraries like React Query. It automatically handles caching, background refetching, retries, and pagination with a single Hook!

import { useQuery } from '@tanstack/react-query'; function UserProfile() { // useQuery handles loading, error, caching automatically! const { data, isLoading, isError } = useQuery({ queryKey: ['user', 'me'], queryFn: () => fetch('https://api.example.com/me').then(res => res.json()) }); if (isLoading) return <ActivityIndicator />; if (isError) return <Text>An error occurred</Text>; return <Text>Hello, {data.name}</Text>; }

Global State Management

For data that isn't from an API (like UI themes, active modals, or simple user preferences), you can use the Context API. For very complex local state, the industry standard is moving towards Zustand (a simpler alternative to Redux).

The Power of the Web Ecosystem

Because React Native uses standard JavaScript, you are not limited to mobile-only libraries. Tools like Axios, React Query, Zustand, and Redux work perfectly on both web and mobile, allowing maximum code reuse.

Animated Example

The Data Fetching Lifecycle

GET /api/user
Wait...
{ "name": "Sara" }
Loading Data...
Hello, Sara!
  1. Component mounts, useEffect triggers a fetch request.
  2. While waiting, loading state is true. The UI shows a spinner.
  3. Data arrives! setUser is called, loading becomes false. UI updates automatically.

Key Takeaways

  • Data fetching requires tracking three states: loading, data, and error.
  • try...catch...finally guarantees loading state is cleared regardless of success or failure.
  • React Query is the modern industry standard for remote data, replacing Redux for API caching.
  • The Context API or Zustand are used for global, non-API state.

Your Turn: Practice

Write a useEffect block that fetches data from https://api.github.com/users/octocat, handles loading and errors, and stores the user data in state.

Lesson 7 · React Native

Native Device Integrations

To build a truly mobile experience, you must access device hardware: the camera, GPS, local storage, and push notifications. React Native excels at this, particularly when using Expo.

Expo SDK: The Ultimate Toolkit

If you are using Expo, you have access to a massive library of pre-built, highly optimized native modules. You simply install them and call Javascript functions.

1. Local Storage: AsyncStorage / SecureStore

To keep a user logged in, you store their authentication token on the device. SecureStore encrypts it natively.

import * as SecureStore from 'expo-secure-store'; async function saveToken(token) { await SecureStore.setItemAsync('auth_token', token); } async function getToken() { return await SecureStore.getItemAsync('auth_token'); }

2. Camera & Image Picker

Allowing users to pick an avatar is a breeze with Expo Image Picker.

import * as ImagePicker from 'expo-image-picker'; const pickImage = async () => { let result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [4, 3], quality: 1, }); if (!result.canceled) { setImage(result.assets[0].uri); } };

Permissions are Mandatory

Mobile operating systems strictly protect user privacy. Before you can access the camera, location, or notifications, you must request permission. Modern libraries handle the native permission dialogs for you.

let { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') { alert('Permission to access location was denied'); return; }

What are Native Modules?

Under the hood, JavaScript cannot directly talk to an iPhone's Bluetooth chip. A Native Module is a bridge consisting of native code (Swift/Objective-C or Kotlin/Java) that performs the hardware task, and a JavaScript interface.

With the JSI (JavaScript Interface) in the New Architecture, JavaScript can directly invoke native methods synchronously, making features like complex camera filters or real-time audio processing incredibly fast.

Background Tasks & TaskManager

Running code when the app is closed or in the background (like location tracking or fetching new messages) requires special handling. Expo provides TaskManager to register background functions that the native OS can wake up and execute.

import * as TaskManager from 'expo-task-manager'; import * as BackgroundFetch from 'expo-background-fetch'; const BACKGROUND_FETCH_TASK = 'background-fetch'; // 1. Define the task outside of your React components TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { const newData = await fetchUpdatesFromServer(); return newData ? BackgroundFetch.BackgroundFetchResult.NewData : BackgroundFetch.BackgroundFetchResult.NoData; }); // 2. Register it in a component BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, { minimumInterval: 15 * 60, // 15 minutes });

Building Your Own Native Modules

What if a company gives you a proprietary Android SDK, but no React Native package exists? You can write a custom Native Module yourself using Swift or Kotlin, wrapping their SDK and exposing a Javascript function. This is React Native's superpower: you are never stuck.

Key Takeaways

  • The Expo SDK provides ready-to-use modules for almost all device features.
  • Always use SecureStore (or encrypted storage) for sensitive data like auth tokens.
  • You must request and verify permissions before accessing privacy-sensitive hardware.
  • Native Modules connect JavaScript to Swift/Kotlin under the hood.

Your Turn: Practice

Name two device features you would need to ask permission for, and two that you wouldn't (e.g. AsyncStorage does not require permission, Camera does).

Lesson 8 · React Native

Flutter vs React Native: The Verdict

You now understand both of the world's leading cross-platform frameworks in depth. A natural question follows: which one should you actually build with? Both are excellent, widely used by top-tier companies, and will serve your career well. But here is the technical comparison.

Architectural Differences

FeatureFlutterReact Native
Core LanguageDart (Strongly Typed, OOP)JavaScript / TypeScript (Dynamic)
Rendering EngineImpeller / Skia (Paints every pixel directly to canvas)Fabric (Instructs OS to render Native UI views)
UI ConsistencyLooks identical on all devicesAdopts the native look of iOS or Android automatically
Performance60-120fps out of the box, highly optimized renderingNear-native (especially with JSI & Hermes)
Ecosystempub.dev (Highly curated for Flutter)npm (Massive, but requires careful selection)

Choose Flutter if...

Choose React Native if...

React Native CLI vs Expo

If you choose React Native, you must decide between React Native CLI or Expo. Expo is a framework wrapped around React Native (similar to Next.js for React). It hides the complex native iOS/Android folders, provides an immense suite of built-in libraries, and manages the build process in the cloud. Today, the official React Native documentation strongly recommends starting with Expo.

Our honest recommendation for a beginner: Start with Flutter (which the majority of our modules focus on). Its all-in-one design and gentle learning curve make it ideal for a first framework. Once you master the fundamentals (State, UI Composition, Networking), picking up React Native will take only a few weeks.

The ultimate truth of Software Engineering

Do not agonise over this choice. The fundamentals — logic, UI as components, state management, asynchronous programming, and clean architecture — are what truly matter. Frameworks are just tools; a skilled developer is not tied to a hammer or a wrench. Pick one, build something real, and launch it.

Animated Example

Choosing your framework

Flutter
Dart · paints pixels
React Native
JS · native parts
Custom UI? → Flutter. Web Background? → React Native.
  1. Flutter (Dart) paints every pixel itself — great for custom design and flawless animations.
  2. React Native (JavaScript) uses the phone's real native components — great if you want native feel or know JS.
  3. Both are excellent. Pick one, build something real, and stop comparing — the fundamentals transfer either way.

Key Takeaways

  • Both Flutter and React Native are top-tier, widely-used frameworks.
  • Flutter offers a highly unified ecosystem and identical custom UIs across devices.
  • React Native offers unparalleled web-synergy, native UI components, and OTA updates.
  • Fundamentals matter far more than the framework — mastering one makes the other trivial.

Your Turn: Practice

Make your decision: which framework will you focus on first, and why? Write three honest sentences based on your background and goals.

Lesson 9 · React Native

Animations & Advanced Performance

To make your app feel truly professional and "native", smooth animations and top-tier performance are non-negotiable. Let's explore the industry-standard tools for achieving 120fps animations and rendering massive lists seamlessly.

React Native Reanimated

The built-in Animated API in React Native is good, but Reanimated (by Software Mansion) is the industry standard for complex, gesture-driven animations. It runs animations directly on the UI thread, bypassing the JS bridge entirely.

import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; function BouncingBox() { const offset = useSharedValue(0); const animatedStyles = useAnimatedStyle(() => { return { transform: [{ translateY: offset.value }], }; }); return ( <> <Animated.View style={[styles.box, animatedStyles]} /> <Button onPress={() => (offset.value = withSpring(Math.random() * 200))} title="Move" /> </> ); }

FlashList: The Ultimate List

While FlatList is fine for simple arrays, it struggles when rendering hundreds of complex items (like a social media feed). Shopify created FlashList to solve this. It recycles views aggressively, offering 5x better performance than FlatList.

import { FlashList } from '@shopify/flash-list'; function Feed({ data }) { return ( <FlashList data={data} renderItem={({ item }) => <Post post={item} />} estimatedItemSize={200} {/* Crucial for performance */} /> ); }

Hermes Engine

React Native now uses Hermes by default. It's an open-source JavaScript engine optimized for React Native. It compiles your JS into bytecode ahead of time (AOT), resulting in faster app launches, decreased memory usage, and smaller app sizes.

Bridgeless Mode & Fabric

With the New Architecture, React Native is moving towards a completely Bridgeless Mode. This means C++ and JS communicate synchronously without serializing JSON across a bridge. Combined with Reanimated and FlashList, React Native performance is virtually indistinguishable from purely native Swift/Kotlin code.

Key Takeaways

  • Use Reanimated for smooth, UI-thread animations and gestures.
  • Replace FlatList with Shopify's FlashList for long or complex lists.
  • Hermes is the default JS engine that pre-compiles code for faster startup.

Your Turn: Practice

Compare Animated.View in React Native with Flutter's AnimatedContainer. What is the fundamental difference in how they execute animations under the hood?

Lesson 10 · React Native

State Management Mastery

As your app grows, passing props down through many layers of components (prop drilling) becomes a nightmare. To solve this, we use global state management tools. Let's look at the industry standards: Redux Toolkit and Zustand.

The Modern Standard: Zustand

Zustand (German for "State") is a small, fast, and scalable barebones state-management solution. It's much simpler than Redux and has become the community favorite.

import { create } from 'zustand'; // 1. Create a store const useStore = create((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })); // 2. Use it anywhere in your app function BearCounter() { const bears = useStore((state) => state.bears); return <Text>{bears} around here ...</Text>; } function Controls() { const increasePopulation = useStore((state) => state.increasePopulation); return <Button onPress={increasePopulation} title="Add Bear" />; }

Redux Toolkit (RTK)

For massive enterprise applications, Redux is still the king. However, raw Redux requires a lot of boilerplate. Redux Toolkit (RTK) is the official, recommended way to write Redux logic.

import { createSlice, configureStore } from '@reduxjs/toolkit'; // 1. Create a "Slice" of state const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; } } }); export const { increment, decrement } = counterSlice.actions; export const store = configureStore({ reducer: { counter: counterSlice.reducer } });

Key Takeaways

  • Stop prop drilling by using a global state manager.
  • Zustand is the modern, lightweight favorite for most applications.
  • Redux Toolkit (RTK) provides a highly structured environment for enterprise-scale apps.

Your Turn: Practice

Compare Zustand's syntax to Redux Toolkit. Notice how Zustand doesn't require a Provider wrapping your app. Which one feels more natural to you?

Lesson 11 · React Native

Testing React Native Apps

Professional apps must be tested before releasing to millions of users. In React Native, testing is broken down into Unit Testing, Component Testing, and End-to-End (E2E) Testing.

Unit & Component Testing: Jest & RNTL

Jest is the standard JavaScript testing framework. Combined with React Native Testing Library (RNTL), you can render your components in memory and interact with them.

import { render, fireEvent } from '@testing-library/react-native'; import HiddenMessage from '../HiddenMessage'; test('shows the children when the button is clicked', () => { const { getByText, queryByText } = render( <HiddenMessage>Hello World</HiddenMessage> ); // The message should be hidden initially expect(queryByText('Hello World')).toBeNull(); // Click the button fireEvent.press(getByText('Show Message')); // The message should now be visible expect(getByText('Hello World')).toBeTruthy(); });

End-to-End (E2E) Testing: Detox

While component tests run quickly in memory, they don't test the real native app. Detox is an E2E testing and automation framework that boots up a real iOS Simulator or Android Emulator, taps the screen like a real user, and asserts results.

describe('Login Flow', () => { it('should login successfully', async () => { await element(by.id('emailInput')).typeText('user@example.com'); await element(by.id('passwordInput')).typeText('secret123'); await element(by.id('loginButton')).tap(); await expect(element(by.text('Welcome User!'))).toBeVisible(); }); });

Key Takeaways

  • Jest is used for testing raw JavaScript functions (Unit Testing).
  • React Native Testing Library is used for testing component rendering and user interactions without a real device.
  • Detox is used for full End-to-End testing on real emulators/simulators.

Your Turn: Practice

Write a simple unit test outline for a `calculateDiscount` function using Jest's `expect` syntax.

Lesson 12 · React Native

Deployment & EAS (Expo Application Services)

Building the app is only half the battle. Releasing it to the Apple App Store and Google Play Store used to be a painful process involving certificates, Xcode, and Android Studio. Today, Expo has revolutionized this.

Expo Application Services (EAS)

EAS is a cloud service by Expo that handles building, submitting, and updating your apps. You do not even need a Mac to build an iOS app anymore!

1. EAS Build

Instead of building the binary (.apk/.aab for Android, .ipa for iOS) on your local machine, EAS compiles it in the cloud.

# Build for iOS simulator (great for testing without a developer account) eas build --profile development --platform ios # Build for production App Store eas build --platform all

2. EAS Submit

Once built, you can upload the binary directly to the App Store Connect and Google Play Console from your terminal.

eas submit --platform all

Over-The-Air (OTA) Updates: EAS Update

This is React Native's greatest superpower. If you find a bug in your JavaScript code, you do not need to wait 3 days for Apple to review your new app version. You can push an OTA update that users download instantly.

eas update --branch production --message "Fixed login bug"

Note: OTA updates can only change JavaScript and assets. If you add a new Native Module (like a new camera SDK), you must do a full App Store release.

Continuous Integration / Continuous Deployment (CI/CD)

In a professional environment, developers set up GitHub Actions to run Jest tests on every commit, and automatically trigger an EAS Build when merging to the main branch.

Key Takeaways

  • EAS Build compiles iOS and Android apps in the cloud, removing the need for local Xcode/Android Studio setups.
  • EAS Submit uploads binaries directly to app stores.
  • EAS Update allows you to push JavaScript bug fixes instantly over-the-air, bypassing app store reviews.

Your Turn: Practice

Explain the difference between a bug that can be fixed via EAS Update and a bug that requires a full EAS Build and App Store submission.