Skip to main content

Managing Component State

1. Using useState for Local State

Example: Basic useState Usage

// Counter component using useState
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default Counter;
Property Description Notes
useState(initialValue) Initializes a state variable with the provided value Can accept any type (object, array, primitive)
[state, setState] Array destructuring to get state and setter function First element is current state, second is the updater
Note: The initial value is only used on the first render. Subsequent re-renders will use the current state value.

Example: State with Object

// Managing user profile with useState
import React, { useState } from 'react';

function UserProfile() {
  const [user, setUser] = useState({
    name: 'John Doe',
    age: 30,
    email: 'john@example.com'
  });

  const updateEmail = (newEmail) => {
    setUser(prevUser => ({
      ...prevUser,
      email: newEmail
    }));
  };

  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
      <p>Email: {user.email}</p>
      <button onClick={() => updateEmail('jane@example.com')}>Update Email</button>
    </div>
  );
}

export default UserProfile;

Example: State with Array

// Managing a todo list with useState
import React, { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: false },
    { id: 2, text: 'Build a project', completed: false }
  ]);

  const addTodo = (text) => {
    const newTodo = {
      id: Date.now(),
      text,
      completed: false
    };
    setTodos([...todos, newTodo]);
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  return (
    <div>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span style={{ textDecoration: todo.completed ? 'line-through' : '' }}>
              {todo.text}
            </span>
            <button onClick={() => toggleTodo(todo.id)}>
              {todo.completed ? 'Undo' : 'Complete'}
            </button>
          </li>
        ))}
      </ul>
      <input
        type="text"
        placeholder="Add a new todo"
        onKeyPress={(e) => {
          if (e.key === 'Enter') {
            addTodo(e.target.value);
            e.target.value = '';
          }
        }}
      />
    </div>
  );
}

export default TodoList;

2. Understanding State Updates

Update Type Description Example
Synchronous Updates State changes are processed synchronously within the same render cycle.
const [count, setCount] = useState(0);

function handleClick() {
  setCount(count + 1); // Synchronous update
  console.log(count); // Logs the previous value
}
Asynchronous Updates State updates may be batched for performance, especially in event handlers.
function handleMultipleClicks() {
  setCount(c => c + 1); // Functional update
  setCount(c => c + 1);
  setCount(c => c + 1); // All updates may be batched
}
Immutable Updates State should be updated immutably to ensure proper re-renders.
const [items, setItems] = useState([]);

// Good: Immutable update
setItems([...items, newItem]);

// Bad: Mutating state directly
items.push(newItem); // Won't trigger re-render
Note: React batches state updates for performance, which can lead to unexpected behavior if relying on synchronous access to updated state values.

3. Using useReducer for Complex State

Example: Basic useReducer Setup

import React, { useReducer } from 'react';

const initialState = {
  count: 0,
  name: 'React'
};

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'SET_NAME':
      return { ...state, name: action.name };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <p>Name: {state.name}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>
        Increment
      </button>
      <button onClick={() => dispatch({ type: 'SET_NAME', name: 'Native' })}>
        Set Name
      </button>
    </div>
  );
}
Property Description Usage
useReducer Hook for managing complex state logic const [state, dispatch] = useReducer(reducer, initialState)
reducer Function that takes state and action, returns new state (state, action) => newState
initialState Initial value of the state Object, function, or primitive
dispatch Function to send actions to the reducer dispatch({ type: 'ACTION' })
Note: useReducer is preferred over useState for complex state logic or when the next state depends on the previous one.

Example: Handling Async Actions

import React, { useReducer } from 'react';

const initialState = {
  loading: false,
  data: null,
  error: null
};

function reducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, data: action.data };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.error };
    default:
      throw new Error();
  }
}

function DataComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const fetchData = async () => {
    dispatch({ type: 'FETCH_START' });
    try {
      const response = await fetch('/api/data');
      const data = await response.json();
      dispatch({ type: 'FETCH_SUCCESS', data });
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', error });
    }
  };

  return (
    <div>
      {state.loading && <p>Loading...</p>}
      {state.error && <p>Error: {state.error}</p>}
      {state.data && <p>Data: {JSON.stringify(state.data)}</p>}
      <button onClick={fetchData}>Fetch Data</button>
    </div>
  );
}

4. Implementing State Lifting

Example: Lifting State Up

// Parent component
function TemperatureConverter() {
  const [temperature, setTemperature] = useState("0");
  const [scale, setScale] = useState("c");

  return (
    <div>
      <BoilingVerdict celsius={parseFloat(temperature)}/>
      <TemperatureInput
        scale={scale}
        value={temperature}
        onChange={(e) => setTemperature(e.target.value)}
      />
    </div>
  );
}

// Child component
function TemperatureInput({ scale, value, onChange }) {
  return (
    <fieldset>
      <legend>Enter temperature in {scale}</legend>
      <input
        value={value}
        onChange={onChange}
      />
    </fieldset>
  );
}

function BoilingVerdict({ celsius }) {
  return <p>{celsius >= 100 ? "Water boils" : "Water does not boil"}</p>;
}
Concept Description Usage
State Lifting Move state to the closest common ancestor of components that need to share it When multiple child components need to access or modify the same state, lift it up to their parent
Props Drilling Passing data through multiple levels of components using props State lifting avoids deep prop drilling by centralizing state in a parent
Note: State lifting is a core React pattern for managing shared state between components.

State Lifting vs Local State

  • Local State: Each component manages its own state
  • State Lifting: Shared state is managed in a common parent

Steps to Implement State Lifting

  1. Identify components that need to share the same state
  2. Find their closest common ancestor
  3. Move the shared state to that ancestor
  4. Pass down state and update functions as props

5. Managing Derived State

Example: Using useMemo for Derived State

import React, { useMemo, useState } from 'react';

function ExpensiveComponent({ items }) {
  const [filter, setFilter] = useState('');

  // Derived state: filtered items
  const filteredItems = useMemo(() => {
    return items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter items"
      />
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}
Property Description Usage
useMemo Optimizes derived state calculations Wrap expensive computations in useMemo to prevent re-calculation
dependencies array Array of values that affect derived state If any dependency changes, recompute the memoized value
Note: useMemo only prevents re-computation if dependencies remain unchanged. It does not prevent the component from re-rendering.

Example: Derived State with Context

import React, { useContext, useMemo } from 'react';

const ThemeContext = React.createContext();

function ThemeProvider({ children, theme }) {
  const value = useMemo(() => ({
    ...theme,
    isDark: theme.mode === 'dark'
  }), [theme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  return useContext(ThemeContext);
}

Derived State vs Direct State

  • Direct State: Stores actual data that changes over time
  • Derived State: Computed from direct state or props
  • useMemo: Prevents recomputation of derived values

6. Handling State Immutability

Approach Description Example
Immutability in React Native State should never be mutated directly. Use functional updates or new object/array creation.
const [count, setCount] = useState(0);
setCount(count + 1); // ✅ Correct
// setCount(count++); ❌ Incorrect - mutates state
Functional Updates Use functional updates to ensure state changes are based on the previous state.
const [items, setItems] = useState([]);
const addItem = (newItem) => {
  setItems(prevItems => [...prevItems, newItem]);
};
Immutable Data Structures Use libraries like Immutable.js or Immer to enforce immutability.
import produce from 'immer';
const [state, setState] = useState({ count: 0 });
const increment = () => {
  setState(produce(draft => {
    draft.count += 1;
  }));
};

Example: Updating Nested State

const [user, setUser] = useState({
  profile: {
    name: 'John',
    age: 30
  }
});

// ❌ Incorrect - direct mutation
user.profile.age = 31;

// ✅ Correct - new object creation
setUser({
  ...user,
  profile: {
    ...user.profile,
    age: 31
  }
});

// ✅ Using Immer for cleaner syntax
setUser(produce(draft => {
  draft.profile.age = 31;
}));
Note: Immutability is crucial for predictable state updates and performance in React Native.

7. Using Functional Updates

Functional updates are a pattern used in React state management to ensure you're working with the most recent state when updating state based on the previous state.

Property Description Usage
useState Hook to manage local state in functional components const [state, setState] = useState(initialState);
Functional Update Pass a function to setState that receives the previous state as an argument setState(prevState => prevState + 1);

Example: Functional Update with useState

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default Counter;
Note: Functional updates are essential when the new state depends on the previous state, especially in asynchronous operations or when multiple updates might occur rapidly.

8. Avoiding State Mutations

Note: In React Native, state mutations can lead to unexpected behavior and bugs that are hard to trace. Always treat state as immutable.
Operation Description Correct Approach Incorrect Approach
Modifying array elements directly Changing an element in an array using index assignment.
const newArray = [...oldArray];
newArray[0] = 'new value';
oldArray[0] = 'new value';
Modifying object properties directly Changing a property of an object using direct assignment. const newObj = { ...oldObj, prop: 'new value' }; oldObj.prop = 'new value';
Pushing to arrays directly Using push() or similar methods that mutate the original array. const newArray = [...oldArray, 'new item']; oldArray.push('new item');

Example: Correct State Update Using Spread Operator

const [items, setItems] = useState([1, 2, 3]);

// Correct way to update state
const addItem = (newItem) => {
  setItems([...items, newItem]);
};

// Correct way to update an item
const updateItem = (index, newValue) => {
  const updatedItems = [...items];
  updatedItems[index] = newValue;
  setItems(updatedItems);
};

Example: Using Immer for Easier Immutable Updates

import produce from 'immer';

const [items, setItems] = useState([1, 2, 3]);

// Using Immer to avoid direct mutation
const addItem = (newItem) => {
  setItems(
    produce(draft => {
      draft.push(newItem);
    })
  );
};

Best Practices for State Mutations

  • Always create a new state object/array instead of mutating the existing one.
  • Use spread operator (...) or array/object methods like slice(), concat(), Object.assign().
  • Consider using libraries like Immer to simplify immutable updates.

9. Managing Multiple State Variables

Example: Using Multiple State Variables

import React, { useState } from 'react';

function UserProfile() {
  const [name, setName] = useState('John');
  const [age, setAge] = useState(25);
  const [email, setEmail] = useState('john@example.com');

  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
      <p>Email: {email}</p>
    </div>
  );
}

export default UserProfile;
State Variable Initial Value Setter Function Description
name 'John' setName User's full name
age 25 setAge User's age in years
email 'john@example.com' setEmail User's email address
Note: Each state variable is managed independently using its own useState hook.

Best Practices for Multiple State Variables

  • Use separate useState calls for each piece of state that changes independently.
  • Group related state variables into a single object if they are updated together.
  • Avoid deeply nested state structures; flatten when possible for better performance and readability.

10. Implementing State Persistence

Example: Using AsyncStorage for Local Storage

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

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

const getData = async (key) => {
  try {
    const value = await AsyncStorage.getItem(key);
    return value ? JSON.parse(value) : null;
  } catch (error) {
    console.error('Error retrieving data:', error);
  }
};

// Usage
storeData('userPreferences', { theme: 'dark', notifications: true });
getData('userPreferences').then(data => console.log(data));
Method Description Notes
setItem(key, value) Stores a key-value pair Value must be stringified
getItem(key) Retrieves a value by key Returns null if not found
removeItem(key) Deletes a key-value pair No return value
getAllKeys() Retrieves all stored keys Async operation
Note: AsyncStorage is not suitable for sensitive data like passwords or tokens. Use secure storage solutions such as react-native-keychain for such cases.

Example: Using Redux Persist with React Native

import { createStore } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';

const persistConfig = {
  key: 'root',
  storage: AsyncStorage,
};

const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = createStore(persistedReducer);
const persistor = persistStore(store);

// In your App component
import { PersistGate } from 'redux-persist/integration/react';

<Provider store={store}>
  <PersistGate loading={null} persistor={persistor}>
    <App />
  </PersistGate>
</Provider>

Comparison: AsyncStorage vs. SQLite vs. Redux Persist

  • AsyncStorage: Simple key-value store, good for small data
  • SQLite: Full database engine, better for structured data
  • Redux Persist: Automatic state persistence for Redux stores

Was this article helpful?