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. | |
| Asynchronous Updates | State updates may be batched for performance, especially in event handlers. | |
| Immutable Updates | State should be updated immutably to ensure proper re-renders. | |
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
- Identify components that need to share the same state
- Find their closest common ancestor
- Move the shared state to that ancestor
- 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. | |
| Functional Updates | Use functional updates to ensure state changes are based on the previous state. | |
| Immutable Data Structures | Use libraries like Immutable.js or Immer to enforce immutability. | |
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. | |
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 likeslice(),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 |
'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
useStatecalls 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>