Managing App State and Lifecycle
1. Using AppState API
| Property | Description | Notes |
|---|---|---|
| AppState.currentState | Current state of the app | String value indicating app state (e.g., 'active', 'background') |
| AppState.addEventListener | Subscribe to app state changes | Event name: 'change' |
Example: Listening to App State Changes
import { AppState } from 'react-native';\n\nconst handleAppStateChange = (nextAppState) => {\n console.log('App state changed to:', nextAppState);\n};\n\nAppState.addEventListener('change', handleAppStateChange);\n\n// Clean up listener when component unmounts\nconst unsubscribe = AppState.addEventListener('change', handleAppStateChange);\nunsubscribe.remove();
Note: On iOS, the app state can be 'active', 'background', or 'inactive'. On Android, it can be 'active', 'background', or 'unknown'.
2. Handling App Background Events
| Event | Description | Usage |
|---|---|---|
| appState | Current state of the app (active, background, inactive) | AppState.addEventListener('change', handleAppStateChange); |
Example: Listening to App State Changes
import { AppState } from 'react-native';\n\nconst handleAppStateChange = (nextAppState) => {\n if (nextAppState === 'background') {\n console.log('App went to background');\n } else if (nextAppState === 'active') {\n console.log('App is active');\n }\n};\n\nAppState.addEventListener('change', handleAppStateChange);
Note: The
appState can be one of: 'active', 'background', or 'inactive'. It's useful for managing resources like network requests or timers.
Warning: On some platforms,
background and inactive states might be indistinguishable.
3. Implementing Component Lifecycle
| Method | Description | Arguments | Usage |
|---|---|---|---|
| constructor() | Initializes the component's state and binds methods | props | Called before component mounts |
| static getDerivedStateFromProps() | Updates state based on props changes | nextProps, prevState | Called before rendering, both on initial mount and updates |
| shouldComponentUpdate() | Controls if component should re-render | nextProps, nextState | Return false to prevent re-rendering |
| getSnapshotBeforeUpdate() | Captures DOM info before update | prevProps, prevState | Return value is passed to componentDidUpdate |
| componentDidUpdate() | Runs after component updates | prevProps, prevState, snapshot | Useful for side effects after updates |
| componentWillUnmount() | Cleans up before component unmounts | None | Useful for clearing timers or subscriptions |
Example: Component Lifecycle Methods
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
static getDerivedStateFromProps(nextProps, prevState) {
return { count: nextProps.value };
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.count !== this.state.count;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
return { scrollTop: this.scrollContainer.scrollTop };
}
componentDidUpdate(prevProps, prevState, snapshot) {
this.scrollContainer.scrollTop = snapshot.scrollTop;
}
componentWillUnmount() {
clearTimeout(this.timer);
}
}
Note: In React 16.3+, use
getDerivedStateFromProps instead of componentWillReceiveProps, which is deprecated.
Warning: Avoid side effects in
shouldComponentUpdate as it can impact performance.
4. Using useEffect for Side Effects
The useEffect hook is a fundamental tool for managing side effects in React Native components. It allows you to perform operations like data fetching, subscriptions, or DOM manipulations after a component renders.
| Parameter | Description | Notes |
|---|---|---|
| effect | A function that contains the side effect logic to run. | Runs after every render by default |
| dependencies | An array of values that the effect depends on. | If omitted, effect runs after every render. If empty array [], effect runs only once. |
Example: Fetching Data
import React, { useState, useEffect } from 'react';
import { View, Text } from 'react-native';
const DataComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Fetch error:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <Text>Loading...</Text>;
return <View><Text>Data: {JSON.stringify(data)}</Text></View>;
};
export default DataComponent;
Example: Setting Up and Cleaning Up Subscriptions
import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
const SubscriptionComponent = () => {
const [time, setTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setTime(new Date());
}, 1000);
// Cleanup function
return () => clearInterval(intervalId);
}, []);
return <View><Text>Current time: {time.toLocaleTimeString()}</Text></View>;
};
export default SubscriptionComponent;
Note: The effect function runs after the component renders, but before the browser paints. If you need to perform a synchronous operation (like DOM measurements), use
useLayoutEffect instead.
Key Points
useEffectis used to handle side effects in functional components.- It runs after every render by default, but can be controlled with a dependency array.
- Return a cleanup function to handle subscriptions or timers when the component unmounts.
5. Managing Component State
| State Type | Description | Usage | Notes |
|---|---|---|---|
| useState | Basic state hook for functional components | const [state, setState] = useState(initialValue); |
Re-renders component on state change |
| useReducer | Advanced state management using reducer function | const [state, dispatch] = useReducer(reducer, initialState); |
Better for complex state logic or when next state depends on previous |
| useContext | Access global state without prop drilling | const value = useContext(MyContext); |
Use with React.createContext |
Example: useState basic usage
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onPress={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Example: useReducer for complex state
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onPress={() => dispatch({ type: 'increment' })}>+</button>
<button onPress={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
Note:
useState is suitable for simple local component state, while useReducer is better for managing more complex state logic or when the next state depends on the previous one.
6. Implementing State Persistence
Example: Using AsyncStorage for Local Storage
import AsyncStorage from '@react-native-async-storage/async-storage';
const saveData = async (key, value) => {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving data:', error);
}
};
const loadData = async (key) => {
try {
const value = await AsyncStorage.getItem(key);
return value ? JSON.parse(value) : null;
} catch (error) {
console.error('Error loading data:', error);
return null;
}
};
// Usage
saveData('userPreferences', { theme: 'dark', notifications: true });
loadData('userPreferences').then(data => console.log(data));
| Method | Description | Notes |
|---|---|---|
| AsyncStorage.setItem(key, value) | Stores data with a key | Value must be stringified |
| AsyncStorage.getItem(key) | Retrieves data by key | Returns null if not found |
| AsyncStorage.removeItem(key) | Deletes data by key | No return value |
| AsyncStorage.clear() | Removes all stored data | Use with caution |
Note: AsyncStorage is not suitable for sensitive data like passwords or tokens. Use secure storage solutions like
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';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
const persistConfig = {
key: 'root',
storage: AsyncStorage,
stateReconciler: autoMergeLevel2
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = createStore(persistedReducer);
const persistor = persistStore(store);
// In your app component
import { PersistGate } from 'redux-persist/integration/react';
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
Comparison: AsyncStorage vs Redux Persist
- AsyncStorage: Low-level storage, manual implementation
- Redux Persist: Higher-level integration with Redux state
- AsyncStorage: No automatic rehydration
- Redux Persist: Automatic state rehydration on app start
7. Handling App Resume
| Event | Description | Usage |
|---|---|---|
| appState | 'active', 'background', 'inactive' |
State of the app |
Example: Listening to App State Changes
import { AppState } from 'react-native';\n\nconst handleAppStateChange = (nextAppState) => {\n if (nextAppState === 'active') {\n console.log('App has come to the foreground');\n } else if (nextAppState === 'background') {\n console.log('App has gone to the background');\n }\n};\n\nAppState.addEventListener('change', handleAppStateChange);\n\n// Don't forget to remove the listener when component unmounts\nconst unsubscribe = AppState.addEventListener('change', handleAppStateChange);\nunsubscribe.remove();
Note: On iOS, the
active state is not always sent when resuming from background.
Key Points
- AppState provides a way to detect when the app state changes.
- Use
AppState.addEventListenerto listen for state changes. - Handle the
'active'state to perform actions when the app resumes.
8. Using useReducer for Complex State
Example: Basic useReducer Setup
import React, { useReducer } from 'react';
const initialState = {
count: 0,
name: 'React Native'
};
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 App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<View>
<Text>Count: {state.count}</Text>
<Text>Name: {state.name}</Text>
<Button title="Increment" onPress={() => dispatch({ type: 'INCREMENT' })} />
<Button title="Set Name" onPress={() => dispatch({ type: 'SET_NAME', name: 'New Name' })} />
</View>
);
}
| Property | Description | Usage |
|---|---|---|
| useReducer | Hook to manage complex state logic | const [state, dispatch] = useReducer(reducer, initialState); |
| reducer | Function that takes state and action, returns new state | function reducer(state, action) { ... } |
| initialState | Initial value for the state | { count: 0, name: 'React Native' } |
| dispatch | Function to send actions to the reducer | dispatch({ type: 'INCREMENT' }) |
Note: useReducer is preferred over useState for complex state logic or when the next state depends on the previous one.
9. Implementing Global State Patterns
Example: Using Redux Toolkit for Global State
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export default store;
| Pattern | Description | Use Case | Pros | Cons |
|---|---|---|---|---|
| Redux Toolkit | Centralized state management with predictable updates | Complex app state, multiple components sharing data | Dev tools, middleware support, easy to test | Steep learning curve, boilerplate for simple cases |
| Context API | Lightweight state sharing across component tree | Simple global state, no need for complex middleware | No external dependencies, built-in to React | Not scalable for large apps, performance issues with frequent updates |
| Zustand | Minimalist state management with flexible API | Small to medium apps, need for simplicity and performance | Easy setup, minimal boilerplate, supports middleware | Less mature ecosystem than Redux |
Note: Choose Redux Toolkit for large-scale apps with complex state logic and need for debugging tools.
Warning: Avoid overusing global state; it can make components tightly coupled and harder to test.
State Management Summary
- Redux Toolkit is best for large, complex applications with many state transitions.
- Context API is suitable for simple state sharing without additional dependencies.
- Zustand offers a balance between simplicity and power for mid-sized projects.
10. Managing Async State Updates
| Method | Description | Usage |
|---|---|---|
| useState | Hook to manage local component state | const [state, setState] = useState(initialValue); |
| useEffect | Hook to handle side effects and async operations | useEffect(() => { /* async logic */ }, [dependencies]); |
| useReducer | Alternative to useState for complex state logic | const [state, dispatch] = useReducer(reducer, initialState); |
Example: Async State Update with useEffect
import React, { useState, useEffect } from 'react';
function AsyncComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error("Fetch error:", error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <div>Loading...</div>;
return <div>Data: {JSON.stringify(data)}</div>;
}
Note: Always handle async operations inside
useEffect to prevent memory leaks and ensure proper cleanup.
Warning: Avoid calling state setters directly inside
useEffect without proper dependency arrays.