What is State Management?
State is the data that can change in your app - like logged-in user info, shopping cart items, or theme preference. State management is how you organize, update, and share this data across your entire app. Think of it like a library's card catalog system - it helps you find and update information efficiently no matter where you are in the building.
Without proper state management, your app becomes like a messy room where you can't find anything. With good state management, it's like a well-organized workspace where everything has its place and is easy to access.
Why State Management Matters
- Avoid Prop Drilling: Pass data to deeply nested components without passing through every level
- Single Source of Truth: One place for each piece of data, no conflicts
- Predictable Updates: Know exactly how and when data changes
- Easier Debugging: Track data flow and find bugs faster
- Better Performance: Only re-render components that need updates
- Testability: Easier to write tests for business logic
- Scalability: Add features without breaking existing code
Types of State
State Types:
┌─────────────────────────────────────────┐
│ 1. Local State (Component State) │
│ - Lives in one component │
│ - Example: Form input, toggle button │
│ - Use: useState, this.state │
│ │
│ 2. Shared State │
│ - Multiple components need access │
│ - Example: User info, shopping cart │
│ - Use: Context, Redux, Provider │
│ │
│ 3. Server State (Remote State) │
│ - Data from backend API │
│ - Example: Posts, products, users │
│ - Use: React Query, SWR │
│ │
│ 4. URL State │
│ - Lives in URL parameters │
│ - Example: Search filters, page num │
│ - Use: Navigation params │
│ │
│ 5. Persistent State │
│ - Survives app restarts │
│ - Example: Settings, auth tokens │
│ - Use: AsyncStorage, SecureStore │
└─────────────────────────────────────────┘
React Native: useState (Local State)
The simplest state management - keeps data inside one component. Like a notebook that only you can see and use.
import React, { useState } from 'react';
import { View, Text, Button, TextInput } from 'react-native';
function Counter() {
// Local state - only this component knows about it
const [count, setCount] = useState(0);
return (
<View>
<Text>Count: {count}</Text>
<Button title="+" onPress={() => setCount(count + 1)} />
<Button title="-" onPress={() => setCount(count - 1)} />
</View>
);
}
// Form with multiple state variables
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const handleSubmit = () => {
console.log('Email:', email);
console.log('Password:', password);
};
return (
<View>
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Email"
/>
<TextInput
value={password}
onChangeText={setPassword}
placeholder="Password"
secureTextEntry={!showPassword}
/>
<Button
title={showPassword ? "Hide" : "Show"}
onPress={() => setShowPassword(!showPassword)}
/>
<Button title="Login" onPress={handleSubmit} />
</View>
);
}
When to use useState:
✓ Data only needed in one component
✓ Simple UI state (toggle, input values)
✓ Short-lived data (temporary selections)
Avoid for:
✗ Data needed by many components
✗ Complex data with many updates
✗ Data that needs to persist
Context API (Shared State)
Context lets you share data across many components without passing props through every level. Think of it like a loudspeaker system - broadcast once, everyone hears it.
import React, { createContext, useContext, useState } from 'react';
// 1. Create Context
const AuthContext = createContext();
// 2. Create Provider Component
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (email, password) => {
setIsLoading(true);
try {
// Call API
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Login failed:', error);
} finally {
setIsLoading(false);
}
};
const logout = () => {
setUser(null);
};
// Value provided to all children
const value = {
user,
isLoading,
login,
logout,
isAuthenticated: user !== null
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// 3. Custom Hook for easy access
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// 4. Wrap App with Provider
function App() {
return (
<AuthProvider>
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
</AuthProvider>
);
}
// 5. Use in Any Component
function ProfileScreen() {
const { user, logout } = useAuth();
return (
<View>
<Text>Welcome, {user.name}!</Text>
<Button title="Logout" onPress={logout} />
</View>
);
}
function HomeScreen() {
const { user, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <LoginScreen />;
}
return <Text>Hello {user.name}</Text>;
}
When to use Context:
✓ Theme (dark/light mode)
✓ User authentication
✓ Language/localization
✓ Global settings
✓ Medium-sized apps
Avoid for:
✗ Frequently changing data (causes re-renders)
✗ Very large apps (use Redux instead)
✗ Complex state logic
Redux (Global State)
Redux is a predictable state container. Think of it like a bank vault - strict rules about deposits and withdrawals, everything tracked, perfectly organized.
Core Concepts
Redux Flow:
┌─────────────────────────────────────┐
│ Component │
│ ↓ dispatch(action) │
│ Store │
│ ↓ sends action to │
│ Reducer │
│ ↓ returns new state │
│ Store │
│ ↓ updates components │
│ Component re-renders │
└─────────────────────────────────────┘
Terms:
- Store: Single object holding all app state
- Action: Plain object describing what happened
- Reducer: Pure function that updates state
- Dispatch: Function to send actions to store
// Install Redux
npm install @reduxjs/toolkit react-redux
// 1. Create Slice (Modern Redux with Redux Toolkit)
import { createSlice } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
total: 0
},
reducers: {
addItem: (state, action) => {
// Redux Toolkit allows "mutating" code
state.items.push(action.payload);
state.total += action.payload.price;
},
removeItem: (state, action) => {
const index = state.items.findIndex(
item => item.id === action.payload.id
);
if (index !== -1) {
state.total -= state.items[index].price;
state.items.splice(index, 1);
}
},
clearCart: (state) => {
state.items = [];
state.total = 0;
}
}
});
export const { addItem, removeItem, clearCart } = cartSlice.actions;
export default cartSlice.reducer;
// 2. Create Store
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './cartSlice';
export const store = configureStore({
reducer: {
cart: cartReducer
}
});
// 3. Provide Store to App
import { Provider } from 'react-redux';
import { store } from './store';
function App() {
return (
<Provider store={store}>
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
</Provider>
);
}
// 4. Use in Components
import { useSelector, useDispatch } from 'react-redux';
import { addItem, removeItem } from './cartSlice';
function ProductScreen({ product }) {
const dispatch = useDispatch();
const handleAddToCart = () => {
dispatch(addItem(product));
};
return (
<View>
<Text>{product.name}</Text>
<Button title="Add to Cart" onPress={handleAddToCart} />
</View>
);
}
function CartScreen() {
const { items, total } = useSelector(state => state.cart);
const dispatch = useDispatch();
return (
<View>
{items.map(item => (
<View key={item.id}>
<Text>{item.name} - ${item.price}</Text>
<Button
title="Remove"
onPress={() => dispatch(removeItem(item))}
/>
</View>
))}
<Text>Total: ${total}</Text>
</View>
);
}
When to use Redux:
✓ Large apps with complex state
✓ State shared across many components
✓ Frequent state updates
✓ Need to track state changes (debugging)
✓ Team familiar with Redux
Avoid for:
✗ Small, simple apps
✗ Mostly server state (use React Query)
✗ Learning curve too steep for team
Flutter: Provider
Provider is the recommended state management solution by Flutter team. Simple, powerful, and efficient.
// Install Provider
dependencies:
provider: ^6.0.0
// 1. Create Model (ChangeNotifier)
import 'package:flutter/foundation.dart';
class CartModel extends ChangeNotifier {
final List<Product> _items = [];
List<Product> get items => _items;
int get itemCount => _items.length;
double get totalPrice => _items.fold(
0,
(total, item) => total + item.price
);
void addItem(Product product) {
_items.add(product);
notifyListeners(); // Tells Flutter to rebuild widgets
}
void removeItem(Product product) {
_items.remove(product);
notifyListeners();
}
void clearCart() {
_items.clear();
notifyListeners();
}
}
// 2. Provide to App
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: MyApp(),
),
);
}
// Multiple providers
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CartModel()),
ChangeNotifierProvider(create: (_) => UserModel()),
ChangeNotifierProvider(create: (_) => ThemeModel()),
],
child: MyApp(),
),
);
}
// 3. Consume in Widgets
class ProductScreen extends StatelessWidget {
final Product product;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(product.name),
ElevatedButton(
onPressed: () {
// Access provider and call method
context.read<CartModel>().addItem(product);
// or: Provider.of<CartModel>(context, listen: false).addItem(product);
},
child: Text('Add to Cart'),
),
],
);
}
}
class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Listen to changes and rebuild when cart updates
final cart = context.watch<CartModel>();
// or: Provider.of<CartModel>(context);
return Column(
children: [
Text('Items: ${cart.itemCount}'),
Text('Total: \$${cart.totalPrice}'),
ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) {
final item = cart.items[index];
return ListTile(
title: Text(item.name),
trailing: IconButton(
icon: Icon(Icons.delete),
onPress: () => cart.removeItem(item),
),
);
},
),
],
);
}
}
// Consumer Widget (rebuilds only this widget)
Consumer<CartModel>(
builder: (context, cart, child) {
return Text('${cart.itemCount} items');
},
)
When to use Provider:
✓ Most Flutter apps (recommended by Flutter team)
✓ Medium to large apps
✓ Need efficiency (only rebuilds what changed)
✓ Want simplicity without sacrificing power
Avoid for:
✗ Very simple apps (use setState)
✗ Very complex apps (consider Bloc or Riverpod)
Flutter: Riverpod
Riverpod is the evolution of Provider - more features, better testability, compile-time safety.
// Install Riverpod
dependencies:
flutter_riverpod: ^2.4.0
// 1. Create Provider
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Simple value provider
final counterProvider = StateProvider<int>((ref) => 0);
// Complex state provider
class CartNotifier extends StateNotifier<List<Product>> {
CartNotifier() : super([]);
void addItem(Product product) {
state = [...state, product]; // Immutable update
}
void removeItem(Product product) {
state = state.where((item) => item.id != product.id).toList();
}
void clearCart() {
state = [];
}
}
final cartProvider = StateNotifierProvider<CartNotifier, List<Product>>(
(ref) => CartNotifier(),
);
// Computed value (automatically updates)
final totalPriceProvider = Provider<double>((ref) {
final cart = ref.watch(cartProvider);
return cart.fold(0, (total, item) => total + item.price);
});
// 2. Wrap App
void main() {
runApp(
ProviderScope( // Instead of MultiProvider
child: MyApp(),
),
);
}
// 3. Use in Widgets
class CounterScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Text('Increment'),
),
],
);
}
}
class CartScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final cart = ref.watch(cartProvider);
final total = ref.watch(totalPriceProvider);
return Column(
children: [
Text('Total: \$$total'),
ListView.builder(
itemCount: cart.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(cart[index].name),
trailing: IconButton(
icon: Icon(Icons.delete),
onPress: () {
ref.read(cartProvider.notifier).removeItem(cart[index]);
},
),
);
},
),
],
);
}
}
When to use Riverpod:
✓ New Flutter projects
✓ Need compile-time safety
✓ Want better testing capabilities
✓ Complex dependency graphs
✓ Prefer immutable state
Avoid for:
✗ Already using Provider (migration effort)
✗ Team unfamiliar with modern patterns
Flutter: Bloc
Bloc (Business Logic Component) separates business logic from UI. Popular for large, complex apps. Think of it like a manager between your data and your UI.
// Install Bloc
dependencies:
flutter_bloc: ^8.1.0
// 1. Define Events (what can happen)
abstract class CartEvent {}
class AddToCart extends CartEvent {
final Product product;
AddToCart(this.product);
}
class RemoveFromCart extends CartEvent {
final Product product;
RemoveFromCart(this.product);
}
class ClearCart extends CartEvent {}
// 2. Define State (what the UI shows)
class CartState {
final List<Product> items;
final double total;
CartState({this.items = const [], this.total = 0});
CartState copyWith({List<Product>? items, double? total}) {
return CartState(
items: items ?? this.items,
total: total ?? this.total,
);
}
}
// 3. Create Bloc (business logic)
class CartBloc extends Bloc<CartEvent, CartState> {
CartBloc() : super(CartState()) {
on<AddToCart>(_onAddToCart);
on<RemoveFromCart>(_onRemoveFromCart);
on<ClearCart>(_onClearCart);
}
void _onAddToCart(AddToCart event, Emitter<CartState> emit) {
final newItems = [...state.items, event.product];
final newTotal = state.total + event.product.price;
emit(state.copyWith(items: newItems, total: newTotal));
}
void _onRemoveFromCart(RemoveFromCart event, Emitter<CartState> emit) {
final newItems = state.items.where((item) => item.id != event.product.id).toList();
final newTotal = state.total - event.product.price;
emit(state.copyWith(items: newItems, total: newTotal));
}
void _onClearCart(ClearCart event, Emitter<CartState> emit) {
emit(CartState());
}
}
// 4. Provide Bloc
BlocProvider(
create: (context) => CartBloc(),
child: MyApp(),
)
// 5. Use in Widgets
BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
return Column(
children: [
Text('Items: ${state.items.length}'),
Text('Total: \$${state.total}'),
ListView.builder(
itemCount: state.items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(state.items[index].name),
trailing: IconButton(
icon: Icon(Icons.delete),
onPress: () {
context.read<CartBloc>().add(
RemoveFromCart(state.items[index])
);
},
),
);
},
),
],
);
},
)
When to use Bloc:
✓ Large, complex apps
✓ Need clear separation of concerns
✓ Team comfortable with reactive programming
✓ Need to track all state changes
✓ Want predictable, testable code
Avoid for:
✗ Small apps (overkill)
✗ Team unfamiliar with streams/reactive programming
✗ Simple state needs
Which One to Choose?
React Native Decision Tree
Start Here:
├── Is state used in ONE component only?
│ └── YES → Use useState
│
├── Is state shared by 2-3 nearby components?
│ └── YES → Lift state up or use useState + props
│
├── Is it user auth, theme, or language?
│ └── YES → Use Context API
│
├── Large app with complex state?
│ └── YES → Use Redux Toolkit
│
└── Mostly server data (API calls)?
└── YES → Use React Query or SWR
Flutter Decision Tree
Start Here:
├── Simple app or learning Flutter?
│ └── YES → Use setState
│
├── Medium-sized app, straightforward state?
│ └── YES → Use Provider
│
├── Want modern, type-safe approach?
│ └── YES → Use Riverpod
│
├── Large app, need strict architecture?
│ └── YES → Use Bloc
│
└── Just need simple global state?
└── YES → Use GetX
Best Practices
- Keep State Minimal: Only store what you need
- Don't Over-Engineer: Start simple, upgrade when needed
- Single Source of Truth: Each piece of data lives in one place
- Immutable Updates: Don't mutate state directly, create new objects
- Separate Concerns: UI components shouldn't contain business logic
- Name Clearly: Use descriptive names for state and actions
- Avoid Nested State: Flatten state structure when possible
- Use Selectors: Don't subscribe to entire store, just what you need
- Handle Loading/Error: Always manage loading and error states
- DevTools: Use Redux DevTools or Flutter DevTools for debugging
Common Mistakes
- Using Redux for Everything: Local state is fine for local data
- Mutating State Directly: Always create new objects/arrays
- Too Much in Context: Context re-renders all consumers when any value changes
- Not Optimizing Re-renders: Use memo, useMemo, useCallback wisely
- Mixing Server and Client State: Use different tools for different state types
- Over-Normalizing Data: Sometimes duplication is okay
- Forgetting to Unsubscribe: Clean up listeners to prevent memory leaks
Ready to Master State Management?
Learn to build scalable, maintainable mobile applications
Explore Hybrid Mobile App Course