Introduction
State management is one of the most crucial aspects of building React Native applications. As your app grows in complexity, managing state effectively becomes essential for maintaining code quality, performance, and user experience. This chapter covers the State Management Basics in React Native, providing you with the knowledge needed to handle application state efficiently.
What is State Management?
State management refers to the process of handling and updating data that changes over time in your application. In React Native, state represents the current condition of your app’s data and UI components. Proper state management ensures that your app remains predictable, maintainable, and performs well.
Types of State
Understanding different types of state is crucial for effective state management:
- Component State (Local State): Data that belongs to a specific component
- Application State (Global State): Data that needs to be shared across multiple components
- Server State: Data fetched from external APIs
- UI State: Data that controls the appearance and behavior of UI elements
Local State Management with useState
The useState hook is the foundation of state management in React Native. It allows you to add state to functional components.
Basic useState Example
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
const CounterApp = () => {
 const [count, setCount] = useState(0);
 const increment = () => {
  setCount(count + 1);
 };
 const decrement = () => {
  setCount(count - 1);
 };
 const reset = () => {
  setCount(0);
 };
 return (
  <View style={styles.container}>
   <Text style={styles.title}>Counter App</Text>
   <Text style={styles.counter}>{count}</Text>
   <View style={styles.buttonContainer}>
    <Button title="+" onPress={increment} />
    <Button title="-" onPress={decrement} />
    <Button title="Reset" onPress={reset} />
   </View>
  </View>
 );
};
const styles = StyleSheet.create({
 container: {
  flex: 1,
  justifyContent: 'center',
  alignItems: 'center',
  backgroundColor: '#f5f5f5',
 },
 title: {
  fontSize: 24,
  fontWeight: 'bold',
  marginBottom: 20,
 },
 counter: {
  fontSize: 48,
  fontWeight: 'bold',
  color: '#007AFF',
  marginBottom: 30,
 },
 buttonContainer: {
  flexDirection: 'row',
  justifyContent: 'space-around',
  width: 200,
 },
});
export default CounterApp;
Managing Complex State with useState
For more complex state objects, you can use the spread operator to update specific properties:
import React, { useState } from 'react';
import { View, Text, TextInput, Button, StyleSheet } from 'react-native';
const UserProfile = () => {
 const [user, setUser] = useState({
  name: '',
  email: '',
  age: '',
  bio: ''
 });
 const updateField = (field, value) => {
  setUser(prevUser => ({
   ...prevUser,
   [field]: value
  }));
 };
 const resetForm = () => {
  setUser({
   name: '',
   email: '',
   age: '',
   bio: ''
  });
 };
 return (
  <View style={styles.container}>
   <Text style={styles.title}>User Profile</Text>
  Â
   <TextInput
    style={styles.input}
    placeholder="Name"
    value={user.name}
    onChangeText={(text) => updateField('name', text)}
   />
  Â
   <TextInput
    style={styles.input}
    placeholder="Email"
    value={user.email}
    onChangeText={(text) => updateField('email', text)}
   />
  Â
   <TextInput
    style={styles.input}
    placeholder="Age"
    value={user.age}
    onChangeText={(text) => updateField('age', text)}
    keyboardType="numeric"
   />
  Â
   <TextInput
    style={styles.input}
    placeholder="Bio"
    value={user.bio}
    onChangeText={(text) => updateField('bio', text)}
    multiline
    numberOfLines={4}
   />
  Â
   <Button title="Reset Form" onPress={resetForm} />
  Â
   <View style={styles.preview}>
    <Text style={styles.previewTitle}>Preview:</Text>
    <Text>Name: {user.name}</Text>
    <Text>Email: {user.email}</Text>
    <Text>Age: {user.age}</Text>
    <Text>Bio: {user.bio}</Text>
   </View>
  </View>
 );
};
const styles = StyleSheet.create({
 container: {
  flex: 1,
  padding: 20,
  backgroundColor: '#fff',
 },
 title: {
  fontSize: 24,
  fontWeight: 'bold',
  marginBottom: 20,
  textAlign: 'center',
 },
 input: {
  borderWidth: 1,
  borderColor: '#ddd',
  padding: 10,
  marginBottom: 10,
  borderRadius: 5,
 },
 preview: {
  marginTop: 20,
  padding: 15,
  backgroundColor: '#f0f0f0',
  borderRadius: 5,
 },
 previewTitle: {
  fontWeight: 'bold',
  marginBottom: 10,
 },
});
export default UserProfile;
Advanced State Management with useReducer
For more complex state logic, useReducer provides a more predictable way to manage state updates:
import React, { useReducer } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
// Define action types
const actionTypes = {
 INCREMENT: 'INCREMENT',
 DECREMENT: 'DECREMENT',
 RESET: 'RESET',
 SET_STEP: 'SET_STEP',
};
// Reducer function
const counterReducer = (state, action) => {
 switch (action.type) {
  case actionTypes.INCREMENT:
   return { ...state, count: state.count + state.step };
  case actionTypes.DECREMENT:
   return { ...state, count: state.count - state.step };
  case actionTypes.RESET:
   return { ...state, count: 0 };
  case actionTypes.SET_STEP:
   return { ...state, step: action.payload };
  default:
   throw new Error(`Unknown action type: ${action.type}`);
 }
};
const AdvancedCounter = () => {
 const initialState = {
  count: 0,
  step: 1,
 };
 const [state, dispatch] = useReducer(counterReducer, initialState);
 return (
  <View style={styles.container}>
   <Text style={styles.title}>Advanced Counter</Text>
   <Text style={styles.counter}>{state.count}</Text>
   <Text style={styles.step}>Step: {state.step}</Text>
  Â
   <View style={styles.buttonContainer}>
    <Button
     title="+"
     onPress={() => dispatch({ type: actionTypes.INCREMENT })}
    />
    <Button
     title="-"
     onPress={() => dispatch({ type: actionTypes.DECREMENT })}
    />
    <Button
     title="Reset"
     onPress={() => dispatch({ type: actionTypes.RESET })}
    />
   </View>
  Â
   <View style={styles.stepContainer}>
    <Text>Change Step:</Text>
    <View style={styles.stepButtons}>
     <Button
      title="1"
      onPress={() => dispatch({ type: actionTypes.SET_STEP, payload: 1 })}
     />
     <Button
      title="5"
      onPress={() => dispatch({ type: actionTypes.SET_STEP, payload: 5 })}
     />
     <Button
      title="10"
      onPress={() => dispatch({ type: actionTypes.SET_STEP, payload: 10 })}
     />
    </View>
   </View>
  </View>
 );
};
const styles = StyleSheet.create({
 container: {
  flex: 1,
  justifyContent: 'center',
  alignItems: 'center',
  backgroundColor: '#f5f5f5',
 },
 title: {
  fontSize: 24,
  fontWeight: 'bold',
  marginBottom: 20,
 },
 counter: {
  fontSize: 48,
  fontWeight: 'bold',
  color: '#007AFF',
  marginBottom: 10,
 },
 step: {
  fontSize: 16,
  color: '#666',
  marginBottom: 30,
 },
 buttonContainer: {
  flexDirection: 'row',
  justifyContent: 'space-around',
  width: 200,
  marginBottom: 30,
 },
 stepContainer: {
  alignItems: 'center',
 },
 stepButtons: {
  flexDirection: 'row',
  justifyContent: 'space-around',
  width: 150,
  marginTop: 10,
 },
});
export default AdvancedCounter;
Lifting State Up
When multiple components need to share state, you should lift the state up to their closest common ancestor:
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
// Child components
const Display = ({ count }) => (
 <View style={styles.display}>
  <Text style={styles.displayText}>Count: {count}</Text>
 </View>
);
const Controls = ({ onIncrement, onDecrement, onReset }) => (
 <View style={styles.controls}>
  <Button title="+" onPress={onIncrement} />
  <Button title="-" onPress={onDecrement} />
  <Button title="Reset" onPress={onReset} />
 </View>
);
const History = ({ history }) => (
 <View style={styles.history}>
  <Text style={styles.historyTitle}>History:</Text>
  {history.map((entry, index) => (
   <Text key={index} style={styles.historyItem}>
    {entry.action} - Value: {entry.value}
   </Text>
  ))}
 </View>
);
// Parent component with lifted state
const LiftedStateExample = () => {
 const [count, setCount] = useState(0);
 const [history, setHistory] = useState([]);
 const addToHistory = (action, value) => {
  setHistory(prev => [...prev, { action, value }]);
 };
 const increment = () => {
  const newCount = count + 1;
  setCount(newCount);
  addToHistory('Increment', newCount);
 };
 const decrement = () => {
  const newCount = count - 1;
  setCount(newCount);
  addToHistory('Decrement', newCount);
 };
 const reset = () => {
  setCount(0);
  addToHistory('Reset', 0);
 };
 return (
  <View style={styles.container}>
   <Text style={styles.title}>Lifted State Example</Text>
   <Display count={count} />
   <Controls
    onIncrement={increment}
    onDecrement={decrement}
    onReset={reset}
   />
   <History history={history} />
  </View>
 );
};
const styles = StyleSheet.create({
 container: {
  flex: 1,
  padding: 20,
  backgroundColor: '#fff',
 },
 title: {
  fontSize: 24,
  fontWeight: 'bold',
  textAlign: 'center',
  marginBottom: 20,
 },
 display: {
  alignItems: 'center',
  marginBottom: 20,
 },
 displayText: {
  fontSize: 24,
  fontWeight: 'bold',
  color: '#007AFF',
 },
 controls: {
  flexDirection: 'row',
  justifyContent: 'space-around',
  marginBottom: 20,
 },
 history: {
  flex: 1,
  backgroundColor: '#f0f0f0',
  padding: 10,
  borderRadius: 5,
 },
 historyTitle: {
  fontSize: 18,
  fontWeight: 'bold',
  marginBottom: 10,
 },
 historyItem: {
  fontSize: 14,
  marginBottom: 5,
 },
});
export default LiftedStateExample;
State Management Patterns
1. Single Source of Truth
Keep your state in a single, centralized location to avoid inconsistencies:
// Good: Centralized state
const App = () => {
 const [appState, setAppState] = useState({
  user: null,
  settings: {},
  notifications: []
 });
 // ... rest of component
};
// Avoid: Multiple scattered states
const App = () => {
 const [user, setUser] = useState(null);
 const [settings, setSettings] = useState({});
 const [notifications, setNotifications] = useState([]);
 // This can become hard to manage
};
2. Immutable State Updates
Always create new state objects instead of mutating existing ones:
// Good: Immutable updates
const addNotification = (newNotification) => {
 setNotifications(prev => [...prev, newNotification]);
};
const updateUserProfile = (updates) => {
 setUser(prev => ({ ...prev, ...updates }));
};
// Avoid: Mutating state directly
const addNotificationWrong = (newNotification) => {
 notifications.push(newNotification); // This is wrong!
 setNotifications(notifications);
};
3. Computed Values
Derive values from state instead of storing them separately:
const ShoppingCart = () => {
 const [items, setItems] = useState([]);
 // Computed values
 const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
 const totalPrice = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
 return (
  <View>
   <Text>Total Items: {totalItems}</Text>
   <Text>Total Price: ${totalPrice.toFixed(2)}</Text>
   {/* ... rest of component */}
  </View>
 );
};
State Management Best Practices
1. Keep State Minimal
Only store what you need in state:
// Good: Minimal state
const [searchQuery, setSearchQuery] = useState('');
const [users, setUsers] = useState([]);
// Computed value
const filteredUsers = users.filter(user =>
 user.name.toLowerCase().includes(searchQuery.toLowerCase())
);
// Avoid: Storing derived data
const [searchQuery, setSearchQuery] = useState('');
const [users, setUsers] = useState([]);
const [filteredUsers, setFilteredUsers] = useState([]); // Redundant!
2. Use Custom Hooks for Complex State Logic
Create reusable custom hooks for common state patterns:
// Custom hook for toggle state
const useToggle = (initialValue = false) => {
 const [value, setValue] = useState(initialValue);
 const toggle = () => setValue(prev => !prev);
 const setTrue = () => setValue(true);
 const setFalse = () => setValue(false);
 return [value, { toggle, setTrue, setFalse }];
};
// Usage
const Component = () => {
 const [isVisible, { toggle, setTrue, setFalse }] = useToggle();
 return (
  <View>
   <Button title="Toggle" onPress={toggle} />
   <Button title="Show" onPress={setTrue} />
   <Button title="Hide" onPress={setFalse} />
   {isVisible && <Text>I'm visible!</Text>}
  </View>
 );
};
3. Custom Hook for API State
import { useState, useEffect } from 'react';
const useApiData = (url) => {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);
 useEffect(() => {
  const fetchData = async () => {
   try {
    setLoading(true);
    const response = await fetch(url);
    if (!response.ok) throw new Error('Network response was not ok');
    const result = await response.json();
    setData(result);
   } catch (err) {
    setError(err.message);
   } finally {
    setLoading(false);
   }
  };
  fetchData();
 }, [url]);
 return { data, loading, error };
};
// Usage
const UserList = () => {
 const { data: users, loading, error } = useApiData('/api/users');
 if (loading) return <Text>Loading...</Text>;
 if (error) return <Text>Error: {error}</Text>;
 return (
  <View>
   {users.map(user => (
    <Text key={user.id}>{user.name}</Text>
   ))}
  </View>
 );
};
Common State Management Mistakes
1. Mutating State Directly
// Wrong
const addItem = (newItem) => {
 items.push(newItem);
 setItems(items);
};
// Correct
const addItem = (newItem) => {
 setItems(prev => [...prev, newItem]);
};
2. Not Using Functional Updates
// Wrong - can cause issues with stale closures
const increment = () => {
 setCount(count + 1);
};
// Correct - always gets the latest value
const increment = () => {
 setCount(prev => prev + 1);
};
3. Over-using State
// Wrong - storing computed values
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
// Correct - computing values
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
When to Use Different State Management Approaches
Use useState when:
- Managing simple local component state
- State is used by a single component
- State logic is straightforward
Use useReducer when:
- State logic is complex with multiple sub-values
- Next state depends on the previous one
- You want to optimize performance with useCallback
Consider external state management (Redux, Context) when:
- Multiple components need the same state
- State needs to persist across navigation
- Complex state updates across the app
Performance Considerations
1. Minimize Re-renders
import React, { useState, useCallback, memo } from 'react';
const ExpensiveComponent = memo(({ onUpdate }) => {
 // This component only re-renders if onUpdate changes
 return <Text>Expensive Component</Text>;
});
const Parent = () => {
 const [count, setCount] = useState(0);
 const [name, setName] = useState('');
 // Memoize callback to prevent unnecessary re-renders
 const handleUpdate = useCallback(() => {
  // Update logic
 }, []);
 return (
  <View>
   <ExpensiveComponent onUpdate={handleUpdate} />
   <Text>{count}</Text>
   <TextInput value={name} onChangeText={setName} />
  </View>
 );
};
2. State Colocation
Keep state as close to where it’s used as possible:
// Good: State close to usage
const UserProfile = () => {
 const [editing, setEditing] = useState(false);
Â
 return (
  <View>
   {editing ? <EditForm /> : <ProfileView />}
   <Button title="Edit" onPress={() => setEditing(true)} />
  </View>
 );
};
// Less optimal: State too high up
const App = () => {
 const [editing, setEditing] = useState(false);
Â
 return (
  <View>
   <Header />
   <UserProfile editing={editing} setEditing={setEditing} />
   <Footer />
  </View>
 );
};
Summary
State management is fundamental to React Native development. Understanding how to effectively manage state using hooks like useState and useReducer is crucial for building maintainable applications. Key takeaways:
- Start Simple: Use useState for simple state management
- Use useReducer: For complex state logic with multiple actions
- Lift State Up: Share state between components by moving it to a common ancestor
- Keep State Minimal: Only store what you need
- Avoid Mutations: Always create new state objects
- Use Custom Hooks: Extract reusable state logic
- Consider Performance: Minimize re-renders and keep state close to usage
As your application grows, you’ll need more sophisticated state management solutions like Redux or Context API, which we’ll cover in later chapters. However, mastering these basics will provide you with a solid foundation for any state management approach you choose to use.
Next Steps
In the next chapter, we’ll explore network requests and API integration, which often involves managing server state alongside your local application state. Understanding the basics covered here will be essential for effectively handling asynchronous data and API responses.
Related Articles:
- React Native vs Flutter in 2024: A Detailed Comparison
- Top 10 React Native Libraries You Must Know in 2024
- React vs React Native: A Deep Dive into Key Differences
- How to Set Up Navigation in React Native : Step-by-Step Guide
- What is Axios? Fetch vs Axios: What’s the Difference?
- React Native Environment Setup: A Beginner’s Step-by-Step Guide
- React Native Web: A Step-by-Step Guide to Cross-Platform Development