useState / useEffect
useState is React Native’s primary local state hook — same API as React on the web. For global state, reach for Context, Zustand, or TanStack Query for server state.
useState, useReducer, context, libraries
EXAMPLE
import { useState, useReducer, createContext, useContext, useMemo } from 'react';
import { View, Text, TextInput, Button, FlatList } from 'react-native';
// 1) useState — basic local state
function Counter() {
const [count, setCount] = useState(0);
return (
<View>
<Text>Count: {count}</Text>
<Button title="+1" onPress={() => setCount(c => c + 1)} />
</View>
);
}
// 2) Multiple states — usually cleaner than one big object
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
async function submit() {
setBusy(true);
setError(null);
try {
await api.login(email, password);
} catch (e) {
setError(e.message);
} finally {
setBusy(false);
}
}
return (
<View style={{ padding: 16, gap: 12 }}>
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Email"
autoCapitalize="none"
keyboardType="email-address"
/>
<TextInput
value={password}
onChangeText={setPassword}
placeholder="Password"
secureTextEntry
/>
<Button title="Sign in" onPress={submit} disabled={busy} />
{error && <Text style={{ color: 'red' }}>{error}</Text>}
</View>
);
}
// 3) Functional updater (safe against stale closures)
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => setSeconds(s => s + 1), 1000);
return () => clearInterval(id);
}, []);
return <Text>{seconds}s</Text>;
}
// 4) Lazy initial state — expensive compute runs ONCE
function BigList() {
const [items, setItems] = useState(() => buildHugeList());
return <FlatList data={items} renderItem={renderItem} keyExtractor={i => i.id} />;
}
// 5) useReducer — for complex state machines
const initial = { count: 0, history: [] };
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1, history: [...state.history, state.count + 1] };
case 'decrement': return { count: state.count - 1, history: [...state.history, state.count - 1] };
case 'reset': return { count: 0, history: [] };
default: return state;
}
}
function Counter2() {
const [state, dispatch] = useReducer(reducer, initial);
return (
<View>
<Text>{state.count}</Text>
<Button title="+1" onPress={() => dispatch({ type: 'increment' })} />
<Button title="-1" onPress={() => dispatch({ type: 'decrement' })} />
<Button title="reset" onPress={() => dispatch({ type: 'reset' })} />
</View>
);
}
// 6) Context — pass state through the tree without prop-drilling
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.me()
.then(setUser)
.finally(() => setLoading(false));
}, []);
const value = useMemo(
() => ({
user,
loading,
signIn: async (email, pw) => setUser(await api.signIn(email, pw)),
signOut: async () => { await api.signOut(); setUser(null); },
}),
[user, loading],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export const useAuth = () => useContext(AuthContext);
// Usage:
// const { user, signIn, signOut } = useAuth();
// 7) State-management libraries
// Zustand — minimal, hook-based
// npm i zustand
import { create } from 'zustand';
const useCartStore = create((set) => ({
items: [],
add: (item) => set(state => ({ items: [...state.items, item] })),
remove: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) })),
clear: () => set({ items: [] }),
total: () => 0, // computed via selector
}));
function Cart() {
const items = useCartStore(s => s.items);
const add = useCartStore(s => s.add);
const remove = useCartStore(s => s.remove);
return (
<FlatList
data={items}
keyExtractor={i => i.id}
renderItem={({ item }) => (
<View>
<Text>{item.name}</Text>
<Button title="Remove" onPress={() => remove(item.id)} />
</View>
)}
/>
);
}
// Jotai — atomic state
// npm i jotai
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
function CounterJotai() {
const [count, setCount] = useAtom(countAtom);
return <Button title={`+1 (${count})`} onPress={() => setCount(c => c + 1)} />;
}
// TanStack Query — server state (the right tool for it)
// npm i @tanstack/react-query
import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query';
const client = new QueryClient();
function App() {
return (
<QueryClientProvider client={client}>
<PostList />
</QueryClientProvider>
);
}
function PostList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['posts'],
queryFn: () => api.getPosts(),
staleTime: 60_000,
});
if (isLoading) return <Text>Loading…</Text>;
if (error) return <Text>Error: {error.message}</Text>;
return (
<FlatList
data={data}
keyExtractor={p => p.id}
renderItem={({ item }) => <Text>{item.title}</Text>}
onRefresh={refetch}
refreshing={isLoading}
/>
);
}
// Mutations
const { mutate: createPost } = useMutation({
mutationFn: api.createPost,
onSuccess: () => client.invalidateQueries({ queryKey: ['posts'] }),
});
// 8) Async storage — persisted state
// npm i @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
async function saveSettings(settings) {
await AsyncStorage.setItem('settings', JSON.stringify(settings));
}
async function loadSettings() {
const raw = await AsyncStorage.getItem('settings');
return raw ? JSON.parse(raw) : null;
}
// Persist with Zustand
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
const useSettingsStore = create(persist(
(set) => ({
theme: 'auto',
notifications: true,
setTheme: (theme) => set({ theme }),
}),
{
name: 'settings',
storage: createJSONStorage(() => AsyncStorage),
},
));
// 9) Form state — reach for a library on big forms
// React Hook Form is the standard
import { useForm, Controller } from 'react-hook-form';
function Form() {
const { control, handleSubmit, formState: { errors } } = useForm();
return (
<View>
<Controller
control={control}
name="email"
rules={{ required: 'Email required', pattern: /\S+@@\S+\.\S+/ }}
render={({ field: { onChange, value, onBlur } }) => (
<TextInput
value={value}
onChangeText={onChange}
onBlur={onBlur}
placeholder="Email"
/>
)}
/>
{errors.email && <Text>{errors.email.message}</Text>}
<Button title="Submit" onPress={handleSubmit(onSubmit)} />
</View>
);
}
// 10) When to use what
// Local component state → useState
// Multi-step / complex transitions → useReducer
// App-wide auth, theme, i18n → Context
// Many components reading/writing same state → Zustand / Jotai
// Server data (fetched from API) → TanStack Query / SWR
// Form state → React Hook Form
// Persistence → AsyncStorage + Zustand persist middleware
// 11) Common bugs
// ❌ Stale closures — capture old state value
// → Use functional updater: setCount(c => c + 1)
// ❌ State setter in render — infinite loop
// → Only set state in event handlers, effects, or useReducer
// ❌ Async state set after unmount
// → Check mounted flag or use useEffect cleanup
// ❌ Storing server data in useState
// → Use TanStack Query — it handles cache, refetch, errors, retries
// ❌ Putting everything in Context
// → Performance: every consumer rerenders. Use Zustand for hot paths.
// 12) Best practices
// ✅ Lift state only as high as needed (closer to the children that use it)
// ✅ Server state ≠ client state — different lifecycles, different tools
// ✅ Persist sparingly — AsyncStorage is slow; don't sync every keystroke
// ✅ Use TypeScript — typed actions + state prevent runtime bugs
// ✅ For complex forms, libraries beat hand-rolled state
// ✅ Profile rerenders with React DevTools when things get slow
Why it matters
useState for local; TanStack Query for server data (it handles cache, refetch, retry); Zustand for shared client state; React Hook Form for forms. Pick by the data’s lifecycle — server data has very different needs than UI state.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { useState, useEffect } from 'react';
const [count, setCount] = useState(0);
useEffect(() => { console.log('count =', count); }, [count]);
Try it Yourself »
Exercise
Hook that holds local component state.
const [n, setN] =
(0);
camelCase, starts with use.
Discussion
Loading…