This episode dissects state, props, and hooks in React Native: useState, useEffect, useRef, useMemo, and useCallback, component composition patterns, and typing props with TypeScript so the code is safe from type errors.

In episode 4 you learned to build static screens. Now those screens need to come alive: responding to button taps, loading data from the server, and storing input values. All of that is controlled by state, props, and hooks — the same concepts as React on the web, but with a mobile flavor. Episode 5 dissects the most frequently used hooks — useState, useEffect, useRef, useMemo, and useCallback — then how to compose components with props and composition, as well as typing props with TypeScript. This is the heart of your app's interactive logic.
useState stores a value that can change and triggers a re-render when it changes:
import React, { useState } from "react";
import { View, Text, Pressable, StyleSheet } from "react-native";
export function Counter() {
const [count, setCount] = useState(0);
return (
<View style={styles.box}>
<Text style={styles.label}>{count}</Text>
<Pressable style={styles.button} onPress={() => setCount(count + 1)}>
<Text>Tambah</Text>
</Pressable>
</View>
);
}Notice the pattern in Counter: useState(0) initializes the state with 0, and setCount(count + 1) updates the value. Every setCount call triggers a re-render with the new value.
A very common mobile pattern is binding a TextInput to state:
const [nama, setNama] = useState("");
<TextInput
value={nama}
onChangeText={setNama}
placeholder="Masukkan nama"
style={styles.input}
/>value={nama} and onChangeText={setNama} make this input fully controlled — the value is always in sync with the nama state. This pattern is the foundation for forms, search, and chat.
useEffect handles side effects: fetching data, subscribing to events, or changing things outside of rendering. On mobile, component lifecycle follows the same mounting and unmounting as React on the web:
import React, { useState, useEffect } from "react";
import { View, Text } from "react-native";
export function UserStatus() {
const [status, setStatus] = useState("memuat...");
useEffect(() => {
let aktif = true;
fetch("https://api.example.com/status")
.then((res) => res.json())
.then((data) => {
if (aktif) setStatus(data.status);
});
return () => {
aktif = false;
};
}, []);
return (
<View>
<Text>Status: {status}</Text>
</View>
);
}The UserStatus pattern above shows two important habits: the empty dependency array [] means the effect only runs once after mount, and the cleanup function aktif = false prevents state updates after the component unmounts — preventing a common error in React Native.
If an effect uses values from props or state, put those values in the dependency array. For example, useEffect(..., [userId]) will re-run the fetch every time userId changes — a basic pattern for detail screens.
useRef stores values that don't trigger a re-render when changed, and the value persists for the lifetime of the component. Two main use cases:
ScrollView for programmatic scrolling.Example of the second use case: timerRef.current stores the interval id set by setInterval and cleared by clearInterval. Since it doesn't trigger a render, storing it in useRef is much cleaner than in useState.
useMemo caches the result of an expensive calculation. Its value is only recomputed when the dependencies change:
const daftarUrut = useMemo(() => {
return daftar.sort((a, b) => b.skor - a.skor);
}, [daftar]);useCallback returns the same function reference as long as the dependencies don't change. This matters when the function is passed as a prop to a memoized child component — preventing unnecessary re-renders in episode 8.
const onKlik = useCallback(() => {
console.log("diklik");
}, []);TypeScript makes the component contract explicit. Define props as an interface and mark optional ones with a question mark:
type ButtonProps = {
title: string;
onPress: () => void;
disabled?: boolean;
};
export function Button({ title, onPress, disabled = false }: ButtonProps) {
return (
<Pressable onPress={onPress} disabled={disabled}>
<Text>{title}</Text>
</Pressable>
);
}In the Button above, disabled? is optional with a default of false. When you call <Button title="Simpan" onPress={simpan} />, the editor immediately validates the props being passed.
type CardProps = {
children: React.ReactNode;
};
export function Card({ children }: CardProps) {
return <View style={styles.card}>{children}</View>;
}With this pattern, <Card><Text>Konten bebas</Text></Card> works for any content. Composition is more flexible than guessing every possible prop combination.
Tip
Separate "dumb" components (which only receive props and render) from "smart" components that hold state. This structure makes logic easy to test and reuse, as you'll see in episode 20.
Episode 5 brought your app to life: state with useState, side effects with useEffect, stable values with useRef, optimization with useMemo and useCallback, and safe prop contracts thanks to TypeScript.
Key takeaways:
useState stores values that trigger a re-render.useEffect handles side effects, with cleanup to prevent updates after unmount.useRef stores stable values that don't trigger a re-render.useMemo caches calculation results; useCallback stabilizes function references.TextInput binds value to state.In the next episode, episode 6, we'll discuss navigation: React Navigation with stack, tab, and drawer, the file-based Expo Router alternative, and deep linking configuration to open specific screens from a URL.