State in React Native works exactly as in React. This chapter covers state with hooks, fetching server data with fetch, storing data on the device, and the state libraries teams commonly use.
State with hooks
Keep component state in useState or useReducer, and move values shared across screens into Context or a state library. TextInput is usually a controlled component, wired to state through value and onChangeText.
import { useState } from "react";
import { Button, Text, TextInput, View } from "react-native";
export function Greeting() {
const [name, setName] = useState("");
const [count, setCount] = useState(0);
return (
<View style={{ padding: 16, gap: 8 }}>
<TextInput value={name} onChangeText={setName} placeholder="Name" />
<Text>Hello, {name || "guest"} ({count})</Text>
<Button title="+1" onPress={() => setCount((c) => c + 1)} />
</View>
);
}
Fetching data with fetch
React Native provides the same fetch API as the web. Cancel requests with AbortController in case the screen goes away before the response arrives.
import { useEffect, useState } from "react";
import { ActivityIndicator, FlatList, Text } from "react-native";
type Post = { id: number; title: string };
const API_URL = process.env.EXPO_PUBLIC_API_URL;
export function Posts() {
const [posts, setPosts] = useState<Post[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
fetch(`${API_URL}/posts`, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<Post[]>;
})
.then(setPosts)
.catch((e) => { if (e.name !== "AbortError") setError(String(e)); })
.finally(() => setLoading(false));
return () => controller.abort();
}, []);
if (loading) return <ActivityIndicator />;
if (error) return <Text>Error: {error}</Text>;
return <FlatList data={posts} keyExtractor={(p) => String(p.id)} renderItem={({ item }) => <Text>{item.title}</Text>} />;
}
A few things to keep in mind:
- In Expo, only environment variables prefixed with
EXPO_PUBLIC_ are inlined into app code. Anyone can unpack an app bundle, so never put secrets there. - From the Android emulator, reach a dev server on your computer at
10.0.2.2 instead of localhost. - Both platforms restrict plain HTTP by default, so serve production APIs over HTTPS.
Server-state libraries
Instead of hand-rolling loading, error, caching and retry logic, a library such as TanStack Query keeps the code short.
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
import { Text } from "react-native";
const queryClient = new QueryClient();
function PostCount() {
const { data, isPending, error } = useQuery({
queryKey: ["posts"],
queryFn: () => fetch("https://example.com/api/posts").then((r) => r.json()),
});
if (isPending) return <Text>Loading</Text>;
if (error) return <Text>Something went wrong</Text>;
return <Text>{data.length} posts</Text>;
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<PostCount />
</QueryClientProvider>
);
}
Storing data on the device
React Native core has no persistent storage, so you pick a library for the job.
AsyncStorage is an unencrypted, asynchronous key-value store for strings, so serialize objects as JSON.
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as SecureStore from "expo-secure-store";
export async function saveSettings(settings: { theme: string }) {
await AsyncStorage.setItem("settings", JSON.stringify(settings));
}
export async function loadSettings() {
const raw = await AsyncStorage.getItem("settings");
return raw ? (JSON.parse(raw) as { theme: string }) : null;
}
// Tokens belong in SecureStore, backed by the iOS Keychain and Android Keystore
export const saveToken = (token: string) => SecureStore.setItemAsync("token", token);
export const loadToken = () => SecureStore.getItemAsync("token");
In an Expo project, install both with npx expo install @react-native-async-storage/async-storage expo-secure-store.
Global state libraries
For client state shared across screens, common choices are Zustand, Redux Toolkit and Jotai. This Zustand store is persisted to AsyncStorage, so it survives app restarts:
import AsyncStorage from "@react-native-async-storage/async-storage";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
type CartState = { items: string[]; add: (id: string) => void; clear: () => void };
export const useCart = create<CartState>()(
persist(
(set) => ({
items: [],
add: (id) => set((s) => ({ items: [...s.items, id] })),
clear: () => set({ items: [] }),
}),
{ name: "cart", storage: createJSONStorage(() => AsyncStorage) },
),
);
Summary
- Use
useState and useReducer for screen state, and Context, Zustand or Redux Toolkit for shared state. - Fetch server data with
fetch; reach for TanStack Query when you need caching and retries. - Store ordinary values in AsyncStorage, sensitive ones in SecureStore or Keychain, and large data sets in SQLite.
- Keep secrets out of the app bundle and serve production APIs over HTTPS.
0 comments
Sign in · Sign in to leave a comment.