iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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);

Discussion

Loading…