DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 14: State Management Basics in React Native

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.

State Management Basics in React Native

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:

  1. Component State (Local State): Data that belongs to a specific component
  2. Application State (Global State): Data that needs to be shared across multiple components
  3. Server State: Data fetched from external APIs
  4. 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:

  1. Start Simple: Use useState for simple state management
  2. Use useReducer: For complex state logic with multiple actions
  3. Lift State Up: Share state between components by moving it to a common ancestor
  4. Keep State Minimal: Only store what you need
  5. Avoid Mutations: Always create new state objects
  6. Use Custom Hooks: Extract reusable state logic
  7. 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:

Scroll to Top