Lesson 6 of 8

State Management & Data Fetching

useState, Context, Redux, React Query, and async storage

State Management in React Native

React Native uses the same state management patterns as React web. You can use useState, useReducer, Context API, or external libraries like Redux, Zustand, or React Query.

Local State with useState

import { useState } from 'react';
import { View, Text, TextInput, Button, FlatList } from 'react-native';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');
  
  const addTodo = () => {
    if (input.trim()) {
      setTodos([
        ...todos,
        { id: Date.now().toString(), text: input, completed: false }
      ]);
      setInput('');
    }
  };
  
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };
  
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
  
  return (
    <View style={styles.container}>
      <TextInput
        value={input}
        onChangeText={setInput}
        placeholder="Add a todo..."
        style={styles.input}
      />
      <Button title="Add" onPress={addTodo} />
      
      <FlatList
        data={todos}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <TodoItem 
            todo={item} 
            onToggle={toggleTodo} 
            onDelete={deleteTodo} 
          />
        )}
      />
    </View>
  );
}

Context API for Global State

// context/AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';

type User = { id: string; name: string; email: string };

type AuthContextType = {
  user: User | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
};

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  
  const login = async (email: string, password: string) => {
    setIsLoading(true);
    try {
      const response = await api.login(email, password);
      setUser(response.user);
      await AsyncStorage.setItem('token', response.token);
    } finally {
      setIsLoading(false);
    }
  };
  
  const logout = async () => {
    setUser(null);
    await AsyncStorage.removeItem('token');
  };
  
  return (
    <AuthContext.Provider value={{ user, isLoading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// Custom hook for using auth
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

// Usage in App.tsx
function App() {
  return (
    <AuthProvider>
      <NavigationContainer>
        <RootNavigator />
      </NavigationContainer>
    </AuthProvider>
  );
}

// Usage in components
function ProfileScreen() {
  const { user, logout } = useAuth();
  
  return (
    <View>
      <Text>Welcome, {user?.name}!</Text>
      <Button title="Logout" onPress={logout} />
    </View>
  );
}

Data Fetching with React Query

// Install: npm install @tanstack/react-query

import { QueryClient, QueryClientProvider, useQuery, useMutation } from '@tanstack/react-query';

const queryClient = new QueryClient();

// Wrap your app
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MainApp />
    </QueryClientProvider>
  );
}

// Fetching data
function UsersList() {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await fetch('https://api.example.com/users');
      return response.json();
    },
    staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
  });
  
  if (isLoading) return <ActivityIndicator />;
  if (error) return <Text>Error: {error.message}</Text>;
  
  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <UserCard user={item} />}
      refreshing={isLoading}
      onRefresh={refetch}
    />
  );
}

// Mutations (POST, PUT, DELETE)
function CreateUser() {
  const mutation = useMutation({
    mutationFn: async (newUser) => {
      const response = await fetch('https://api.example.com/users', {
        method: 'POST',
        body: JSON.stringify(newUser),
      });
      return response.json();
    },
    onSuccess: () => {
      // Invalidate and refetch users list
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
  
  return (
    <Button
      title="Create User"
      onPress={() => mutation.mutate({ name: 'John' })}
      disabled={mutation.isPending}
    />
  );
}

AsyncStorage - Persistent Storage

// Install: npx expo install @react-native-async-storage/async-storage

import AsyncStorage from '@react-native-async-storage/async-storage';

// Store data
const storeData = async (key: string, value: any) => {
  try {
    const jsonValue = JSON.stringify(value);
    await AsyncStorage.setItem(key, jsonValue);
  } catch (e) {
    console.error('Error storing data:', e);
  }
};

// Retrieve data
const getData = async (key: string) => {
  try {
    const jsonValue = await AsyncStorage.getItem(key);
    return jsonValue != null ? JSON.parse(jsonValue) : null;
  } catch (e) {
    console.error('Error reading data:', e);
    return null;
  }
};

// Remove data
const removeData = async (key: string) => {
  try {
    await AsyncStorage.removeItem(key);
  } catch (e) {
    console.error('Error removing data:', e);
  }
};

// Custom hook for persistent state
function usePersistentState<T>(key: string, defaultValue: T) {
  const [state, setState] = useState<T>(defaultValue);
  const [isLoading, setIsLoading] = useState(true);
  
  // Load on mount
  useEffect(() => {
    getData(key).then((value) => {
      if (value !== null) setState(value);
      setIsLoading(false);
    });
  }, [key]);
  
  // Save on change
  const setPersistentState = async (value: T) => {
    setState(value);
    await storeData(key, value);
  };
  
  return [state, setPersistentState, isLoading] as const;
}

// Usage
function Settings() {
  const [theme, setTheme, loading] = usePersistentState('theme', 'light');
  
  if (loading) return <ActivityIndicator />;
  
  return (
    <Switch
      value={theme === 'dark'}
      onValueChange={(value) => setTheme(value ? 'dark' : 'light')}
    />
  );
}

Zustand - Simple State Management

// Install: npm install zustand

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

// Simple store
interface CounterStore {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

const useCounterStore = create<CounterStore>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

// With persistence
interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  clearCart: () => void;
}

const useCartStore = create<CartStore>()(
  persist(
    (set) => ({
      items: [],
      addItem: (item) => set((state) => ({ 
        items: [...state.items, item] 
      })),
      removeItem: (id) => set((state) => ({ 
        items: state.items.filter((i) => i.id !== id) 
      })),
      clearCart: () => set({ items: [] }),
    }),
    {
      name: 'cart-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);

// Usage in components
function CartButton() {
  const itemCount = useCartStore((state) => state.items.length);
  const clearCart = useCartStore((state) => state.clearCart);
  
  return (
    <View>
      <Text>Cart: {itemCount} items</Text>
      <Button title="Clear" onPress={clearCart} />
    </View>
  );
}

📊 When to Use What

  • • useState: Local component state
  • • Context: Theme, auth, app-wide settings
  • • React Query: Server state, API data, caching
  • • Zustand/Redux: Complex client state, shopping carts
  • • AsyncStorage: Persistent data (tokens, preferences)

API Service Pattern

// services/api.ts
const BASE_URL = 'https://api.example.com';

class ApiClient {
  private token: string | null = null;
  
  setToken(token: string | null) {
    this.token = token;
  }
  
  private async request<T>(
    endpoint: string, 
    options: RequestInit = {}
  ): Promise<T> {
    const headers: HeadersInit = {
      'Content-Type': 'application/json',
      ...(this.token && { Authorization: `Bearer ${this.token}` }),
      ...options.headers,
    };
    
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      ...options,
      headers,
    });
    
    if (!response.ok) {
      throw new Error(`API Error: ${response.status}`);
    }
    
    return response.json();
  }
  
  // GET request
  get<T>(endpoint: string) {
    return this.request<T>(endpoint);
  }
  
  // POST request
  post<T>(endpoint: string, data: any) {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }
  
  // PUT request
  put<T>(endpoint: string, data: any) {
    return this.request<T>(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }
  
  // DELETE request
  delete<T>(endpoint: string) {
    return this.request<T>(endpoint, { method: 'DELETE' });
  }
}

export const api = new ApiClient();

// Usage
const users = await api.get<User[]>('/users');
const newUser = await api.post<User>('/users', { name: 'John' });