Managing State Using useState and useReducer in React Native
Course: React Native Training
Topic: Managing State Using useState and useReducer
Course Link: React Native Training at JustAcademy
Demo Registration: Register for a Course Demo
JustAcademy Website: Visit JustAcademy
1. Introduction to State Management in React Native
State management is an important concept in React Native because applications need to store, update, and display changing information. Examples include form inputs, shopping cart items, selected tabs, counters, login status, loading indicators, and API response data.
React Native uses React's state management features to update the user interface when data changes. Two commonly used Hooks for managing component state are useState and useReducer.
- useState: Manages individual state values or relatively simple state objects.
- useReducer: Manages state through a reducer function that processes actions and returns the next state.
Choosing the appropriate Hook helps make application code easier to understand, maintain, test, and debug.
1.1 What Is State?
State is data that a component remembers between renders. When state changes, React renders the component again so that the interface reflects the latest state.
For example, a counter component may store a number, while a registration form may store a user's name, email address, and password.
const [count, setCount] = useState(0);
In this example, count represents the current value, and setCount is the function used to request an update.
1.2 Examples of State in React Native
| Application Feature |
Example State |
Purpose |
| Counter |
count |
Stores the current count. |
| Login form |
email, password |
Stores entered form values. |
| Shopping cart |
cartItems |
Stores selected products. |
| API request |
loading, error, data |
Tracks request status and results. |
| Accordion |
expanded |
Controls whether content is visible. |
| Task manager |
tasks |
Stores, updates, and removes tasks. |
| Authentication |
user |
Represents the current user state. |
2. Understanding React Hooks
Hooks are functions that allow React function components to use features such as state, context, and effects. React Native applications commonly use Hooks to manage screen data and respond to user interactions.
2.1 What Is a Hook?
A Hook is a special React function that provides access to React features from a function component or a custom Hook.
Common Hooks include:
useState for state variables.
useReducer for reducer-based state management.
useEffect for synchronizing with external systems.
useContext for reading shared context values.
useMemo for memoizing calculated values when appropriate.
2.2 Rules of Hooks
- Call Hooks at the top level of a React function component or custom Hook.
- Do not call Hooks inside loops, conditions, or nested ordinary functions.
- Do not call Hooks from regular JavaScript functions that are not React components or custom Hooks.
- Use the same Hook order on every render.
- Use state setter functions or reducer dispatch actions to update state instead of directly modifying existing state values.
3. Introduction to useState
3.1 What Is useState?
useState is a React Hook that allows a component to store a state value and update it in response to events or other application logic.
It is especially useful for simple state updates, such as changing text, incrementing a counter, toggling a switch, selecting a category, or tracking whether a modal is visible.
3.2 Syntax of useState
import { useState } from 'react';
const [state, setState] = useState(initialValue);
The syntax contains three important parts:
- state: The current state value.
- setState: The function used to update the state.
- initialValue: The value used to initialize the state during the component's initial render.
3.3 How useState Works
- The component renders for the first time.
- React initializes the state using the provided initial value.
- The user performs an action, such as pressing a button or typing into a text field.
- The event handler calls the state setter.
- React schedules a state update and renders the component again when needed.
- The updated interface displays the new state.
4. Creating a Counter Using useState
A counter is one of the simplest examples of state management. It stores a number and changes that number when the user presses a button.
4.1 Complete Counter Example
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
export default function CounterScreen() {
const [count, setCount] = useState(0);
function increment() {
setCount(currentCount => currentCount + 1);
}
function decrement() {
setCount(currentCount => currentCount - 1);
}
function reset() {
setCount(0);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Counter App</Text>
<Text style={styles.count}>Count: {count}</Text>
<Button title="Increment" onPress={increment} />
<Button title="Decrement" onPress={decrement} />
<Button title="Reset" onPress={reset} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20
},
title: {
fontSize: 24,
marginBottom: 20
},
count: {
fontSize: 32,
marginBottom: 20
}
});
4.2 Explanation
useState(0) initializes the counter to zero.
count holds the current number.
setCount() requests a state update.
- The increment function uses an updater function to calculate the next count from the previous count.
- The decrement function subtracts one from the previous count.
- The reset function sets the counter back to zero.
- The
Text component displays the current count.
4.3 Why Use the Functional State Updater?
When the next state depends on the previous state, use the functional updater form.
setCount(previousCount => previousCount + 1);
This is safer than relying on a state variable captured by an event handler when several updates may be queued before the next render.
For example, if three increments should be applied in one event handler, use:
setCount(count => count + 1);
setCount(count => count + 1);
setCount(count => count + 1);
React processes the updater functions in sequence, so the counter increases by three.
5. Managing Different Types of State with useState
5.1 String State
String state is useful for storing names, search queries, labels, and text field values.
const [name, setName] = useState('');
function updateName(value) {
setName(value);
}
5.2 Boolean State
Boolean state stores either true or false. It is often used for toggles, modal visibility, password visibility, and loading indicators.
const [isVisible, setIsVisible] = useState(false);
function toggleVisibility() {
setIsVisible(currentValue => !currentValue);
}
5.3 Array State
Arrays can store lists of tasks, products, notifications, or selected options. Update arrays immutably instead of changing the existing array.
const [items, setItems] = useState([]);
function addItem(item) {
setItems(currentItems => [...currentItems, item]);
}
function removeItem(itemId) {
setItems(currentItems =>
currentItems.filter(item => item.id !== itemId)
);
}
5.4 Object State
Object state is useful for grouping related values, such as profile information or a form's fields.
const [profile, setProfile] = useState({
name: '',
email: '',
city: ''
});
function updateName(name) {
setProfile(currentProfile => ({
...currentProfile,
name
}));
}
The spread operator preserves the other properties while replacing the name. React state updates do not automatically merge object properties.
6. Building a Form Using useState
Forms often contain several related state values. The following example manages a user's name and email using separate state variables.
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
Button,
Alert,
StyleSheet
} from 'react-native';
export default function RegistrationScreen() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
function handleSubmit() {
if (!name.trim() || !email.trim()) {
Alert.alert('Validation Error', 'Enter your name and email.');
return;
}
Alert.alert('Registration', `Welcome, ${name}!`);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Registration Form</Text>
<TextInput
style={styles.input}
placeholder="Enter your name"
value={name}
onChangeText={setName}
/>
<TextInput
style={styles.input}
placeholder="Enter your email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<Button title="Submit" onPress={handleSubmit} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center'
},
title: {
fontSize: 24,
marginBottom: 20
},
input: {
borderWidth: 1,
borderColor: '#999',
padding: 12,
marginBottom: 15,
borderRadius: 6
}
});
6.1 Important Form Concepts
- Controlled input: The field value is supplied by React state.
- onChangeText: Receives the new text value in React Native's TextInput.
- Validation: Checks whether required fields contain acceptable values.
- Submission: Uses the current state to continue the registration process.
Client-side validation improves usability but does not replace server-side validation. A production form should also handle API requests, duplicate accounts, network failures, and submission loading states.
7. Updating Objects and Arrays Immutably
7.1 What Is Immutability?
Immutability means creating a new object or array instead of modifying an existing state object directly. React relies on state identity and rendering behavior, so mutating existing state can lead to confusing or incorrect results.
7.2 Incorrect Object Update
// Avoid mutating existing state
profile.name = 'Aarav';
setProfile(profile);
This modifies the existing object rather than creating a new one. The update may not trigger the expected render because the object reference has not changed.
7.3 Correct Object Update
setProfile(currentProfile => ({
...currentProfile,
name: 'Aarav'
}));
7.4 Incorrect Array Update
// Avoid mutating an existing state array
items.push(newItem);
setItems(items);
7.5 Correct Array Update
setItems(currentItems => [...currentItems, newItem]);
For deletion, use filter(). For transforming values, use map(). For adding an item, use a new array created with spread syntax or another immutable approach.
8. Advantages and Limitations of useState
8.1 Advantages
- Easy to learn and implement.
- Requires relatively little code for simple state.
- Works well for individual values and small objects.
- Fits common interactions such as toggles, text fields, and counters.
- Can be used alongside other Hooks.
8.2 Limitations
- Many state variables can become difficult to coordinate.
- Complex update logic may become duplicated across event handlers.
- Related state changes can be spread across different functions.
- Care is required when updates depend on previous state.
- Large objects and arrays require careful immutable updates.
These limitations do not mean that useState is unsuitable for larger components. They indicate when grouping state and centralizing update logic with a reducer may improve maintainability.
9. Introduction to useReducer
9.1 What Is useReducer?
useReducer is a React Hook that manages state through a reducer function. Instead of directly describing every state change inside individual event handlers, the component dispatches actions. The reducer determines the next state based on the current state and the action.
It is useful when several actions update related state or when the update rules are complex enough to benefit from being kept in one place.
9.2 Syntax of useReducer
import { useReducer } from 'react';
const [state, dispatch] = useReducer(
reducer,
initialState
);
The syntax contains three key elements:
- state: The current state value.
- dispatch: A function that sends an action to the reducer.
- reducer: A function that calculates and returns the next state.
- initialState: The initial state value, unless an initializer function is provided.
9.3 What Is a Reducer Function?
A reducer is a function that receives the current state and an action and returns the next state.
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
The reducer should be pure. It should not directly modify the existing state, make network requests, write to storage, or perform other side effects. It should calculate the next state using its inputs.
10. Understanding Actions and Dispatch
10.1 What Is an Action?
An action describes an event or requested change. It is commonly represented by an object containing a type property and optional payload data.
{ type: 'increment' }
{ type: 'addItem', payload: { id: 1, name: 'Notebook' } }
{ type: 'removeItem', payload: 1 }
10.2 What Is Dispatch?
The dispatch function sends an action to the reducer.
dispatch({ type: 'increment' });
React calls the reducer with the current state and the dispatched action, stores the resulting state, and renders the component with the updated value when needed.
10.3 The State Update Flow
- The user performs an action, such as pressing a button.
- The event handler calls
dispatch().
- The action describes the requested change.
- The reducer receives the current state and action.
- The reducer returns a new state without mutating the existing state.
- React updates the component using the next state.
11. Creating a Counter Using useReducer
11.1 Complete Example
import React, { useReducer } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
const initialState = {
count: 0
};
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + 1
};
case 'decrement':
return {
...state,
count: state.count - 1
};
case 'reset':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
export default function CounterReducerScreen() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<View style={styles.container}>
<Text style={styles.title}>Reducer Counter</Text>
<Text style={styles.count}>Count: {state.count}</Text>
<Button
title="Increment"
onPress={() => dispatch({ type: 'increment' })}
/>
<Button
title="Decrement"
onPress={() => dispatch({ type: 'decrement' })}
/>
<Button
title="Reset"
onPress={() => dispatch({ type: 'reset' })}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20
},
title: {
fontSize: 24,
marginBottom: 20
},
count: {
fontSize: 32,
marginBottom: 20
}
});
11.2 Explanation
initialState defines the starting counter value.
counterReducer defines how each action changes the state.
useReducer() returns the current state and dispatch function.
- Each button dispatches a named action.
- The reducer returns a new object containing the next count.
- The component reads the value from
state.count.
11.3 Why Return a New Object?
The reducer returns a new state object instead of modifying the existing one. This follows React's immutable state update principles and makes state transitions easier to understand and test.
12. Building a Task Manager Using useReducer
A task manager is a useful example of reducer-based state management because adding, completing, and deleting tasks are different actions that update the same collection.
12.1 Define the Initial State
const initialTasks = [
{ id: '1', title: 'Learn React Native', completed: false },
{ id: '2', title: 'Practice Hooks', completed: false }
];
12.2 Create the Task Reducer
function tasksReducer(state, action) {
switch (action.type) {
case 'add': {
const title = action.payload.title.trim();
if (!title) {
return state;
}
return [
...state,
{
id: action.payload.id,
title,
completed: false
}
];
}
case 'toggle':
return state.map(task =>
task.id === action.payload
? { ...task, completed: !task.completed }
: task
);
case 'delete':
return state.filter(task => task.id !== action.payload);
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
12.3 Use the Reducer in a Component
import React, { useReducer, useState } from 'react';
import {
View,
Text,
TextInput,
Button,
FlatList,
Pressable,
StyleSheet
} from 'react-native';
const initialTasks = [
{ id: '1', title: 'Learn React Native', completed: false },
{ id: '2', title: 'Practice Hooks', completed: false }
];
function tasksReducer(state, action) {
switch (action.type) {
case 'add': {
const title = action.payload.title.trim();
if (!title) return state;
return [
...state,
{
id: action.payload.id,
title,
completed: false
}
];
}
case 'toggle':
return state.map(task =>
task.id === action.payload
? { ...task, completed: !task.completed }
: task
);
case 'delete':
return state.filter(task => task.id !== action.payload);
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
export default function TaskManagerScreen() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
const [title, setTitle] = useState('');
function addTask() {
const trimmedTitle = title.trim();
if (!trimmedTitle) return;
dispatch({
type: 'add',
payload: {
id: `${Date.now()}-${Math.random()}`,
title: trimmedTitle
}
});
setTitle('');
}
return (
<View style={styles.container}>
<Text style={styles.heading}>Task Manager</Text>
<TextInput
style={styles.input}
placeholder="Enter a task"
value={title}
onChangeText={setTitle}
/>
<Button title="Add Task" onPress={addTask} />
<FlatList
data={tasks}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View style={styles.task}>
<Pressable
style={styles.taskText}
onPress={() =>
dispatch({ type: 'toggle', payload: item.id })
}
>
<Text>
{item.completed ? '✓ ' : '○ '}
{item.title}
</Text>
</Pressable>
<Button
title="Delete"
onPress={() =>
dispatch({ type: 'delete', payload: item.id })
}
/>
</View>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20
},
heading: {
fontSize: 24,
marginBottom: 16
},
input: {
borderWidth: 1,
borderColor: '#999',
borderRadius: 6,
padding: 12,
marginBottom: 12
},
task: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 10
},
taskText: {
flex: 1,
padding: 8
}
});
This example uses Math.random() only as part of a simple demonstration ID. In production applications, use a reliable ID-generation strategy appropriate to your data model, and avoid relying on a timestamp alone when IDs must be unique.
12.4 Features of the Task Manager
- Add a new task.
- Mark a task as completed or incomplete.
- Delete a task.
- Store the task list in reducer-managed state.
- Keep the update rules in one reducer function.
13. Comparing useState and useReducer
Both Hooks manage component state, but they organize updates differently.
| Feature |
useState |
useReducer |
| State update method |
Calls a setter function. |
Dispatches an action to a reducer. |
| Best suited for |
Simple or independent state updates. |
Complex or closely related state transitions. |
| Code structure |
Update logic often lives in event handlers. |
Update logic is centralized in the reducer. |
| Initial setup |
Usually shorter. |
Requires a reducer and action definitions. |
| Testing |
Often tested through component behavior. |
Reducer logic can be tested independently. |
| State updates |
Uses values or updater functions. |
Uses state and action to return the next state. |
| Example |
Modal visibility, search text, counter. |
Task list, multi-step form, complex cart logic. |
| Can they be combined? |
Yes. A component can use both Hooks for different parts of its state. |
13.1 When Should You Use useState?
- When the state is simple and independent.
- When only a few values need to be updated.
- When the update logic is easy to understand in the event handler.
- When building a toggle, simple form input, or basic counter.
13.2 When Should You Use useReducer?
- When many event handlers update the same state object or collection.
- When a single action must update several related state fields.
- When state transitions have multiple rules.
- When centralized logic would make debugging and testing easier.
- When you want to represent changes as explicit actions.
Neither Hook is universally better. Select the simplest approach that keeps your state logic understandable.
14. Managing API Loading, Success, and Error States
Applications often need to track several related states while loading data from an API. A reducer can be useful when the request has distinct loading, success, and failure transitions.
14.1 Define the Initial State
const initialState = {
status: 'idle',
data: null,
error: null
};
14.2 Define the Reducer
function requestReducer(state, action) {
switch (action.type) {
case 'requestStarted':
return {
status: 'loading',
data: null,
error: null
};
case 'requestSucceeded':
return {
status: 'success',
data: action.payload,
error: null
};
case 'requestFailed':
return {
status: 'error',
data: null,
error: action.payload
};
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
14.3 Use the Reducer with an API Request
import React, { useEffect, useReducer } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
const initialState = {
status: 'idle',
data: null,
error: null
};
function requestReducer(state, action) {
switch (action.type) {
case 'requestStarted':
return { status: 'loading', data: null, error: null };
case 'requestSucceeded':
return { status: 'success', data: action.payload, error: null };
case 'requestFailed':
return { status: 'error', data: null, error: action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
export default function UserScreen() {
const [state, dispatch] = useReducer(requestReducer, initialState);
useEffect(() => {
const controller = new AbortController();
async function loadUser() {
dispatch({ type: 'requestStarted' });
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/users/1',
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error('Unable to load user data');
}
const user = await response.json();
dispatch({
type: 'requestSucceeded',
payload: user
});
} catch (error) {
if (error.name !== 'AbortError') {
dispatch({
type: 'requestFailed',
payload: error.message || 'An unexpected error occurred'
});
}
}
}
loadUser();
return () => controller.abort();
}, []);
if (state.status === 'loading' || state.status === 'idle') {
return <ActivityIndicator size="large" />;
}
if (state.status === 'error') {
return <Text>Error: {state.error}</Text>;
}
return (
<View>
<Text>Name: {state.data.name}</Text>
<Text>Email: {state.data.email}</Text>
</View>
);
}
This example demonstrates request-state management. In a real application, use an API appropriate to your project, handle retries and empty results when needed, and consider whether the request should be cancelled or ignored when the screen is no longer active.
15. Combining useState and useReducer
React does not require an application to use only one state Hook. You can use useState for small independent values and useReducer for complex state transitions within the same component.
import React, { useReducer, useState } from 'react';
import { View, Text, TextInput, Button } from 'react-native';
const initialState = {
tasks: []
};
function reducer(state, action) {
switch (action.type) {
case 'add':
return {
...state,
tasks: [...state.tasks, action.payload]
};
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
export default function TaskScreen() {
const [text, setText] = useState('');
const [state, dispatch] = useReducer(reducer, initialState);
function addTask() {
const title = text.trim();
if (!title) return;
dispatch({
type: 'add',
payload: {
id: `${Date.now()}-${Math.random()}`,
title
}
});
setText('');
}
return (
<View>
<TextInput value={text} onChangeText={setText} />
<Button title="Add Task" onPress={addTask} />
{state.tasks.map(task => (
<Text key={task.id}>{task.title}</Text>
))}
</View>
);
}
Here, useState manages the text input, while useReducer manages the task collection.
16. Common Mistakes and Troubleshooting
| Problem |
Cause |
Solution |
| UI does not reflect the expected update |
Existing object or array was mutated. |
Create and return a new object or array. |
| Counter skips or loses updates |
Updates depend on a captured state value. |
Use the functional state updater. |
| Other object fields disappear |
The new object replaces the previous object. |
Use the spread operator to preserve required fields. |
| Reducer state becomes undefined |
A reducer branch does not return a valid next state. |
Return a state value for every supported action. |
| Unknown actions cause unexpected behavior |
The reducer does not handle an action type. |
Use a default branch and handle unknown actions deliberately. |
| API loading indicator never stops |
Success or error transitions are missing. |
Dispatch success or failure actions and handle all outcomes. |
| State update logic is difficult to follow |
Many related updates are scattered across handlers. |
Consider grouping related state transitions in a reducer. |
| State is updated directly |
Existing state is treated as mutable data. |
Use setters or dispatch and immutable updates. |
| Hook behaves inconsistently |
A Hook is called inside a condition or loop. |
Call Hooks at the top level of the component. |
17. Best Practices for State Management
- Use
useState for straightforward state and useReducer when update logic benefits from centralization.
- Keep state as small and simple as practical.
- Avoid storing values that can be calculated from existing state during rendering.
- Do not mutate state objects or arrays.
- Use functional state updates when the next value depends on the previous value.
- Give reducer actions clear, descriptive names such as
addTask, deleteTask, and requestSucceeded.
- Keep reducer functions pure and predictable.
- Do not perform API requests, storage operations, or other side effects inside a reducer.
- Handle loading, success, empty, and error states where relevant.
- Keep unrelated state independent when that improves clarity.
- Use stable, unique keys when rendering lists.
- Test reducers separately when their logic is sufficiently complex.
- Avoid moving all state into a reducer simply because the application has many components.
- Consider Context or a dedicated state-management library only when state-sharing requirements justify it.
18. Mini Project: Shopping Cart State Management
18.1 Project Overview
Create a shopping cart screen that allows users to add products, remove products, update quantities, and view the total price. Use a reducer to keep cart operations consistent.
18.2 Initial State
const initialCartState = {
items: []
};
18.3 Cart Reducer
function cartReducer(state, action) {
switch (action.type) {
case 'addItem': {
const product = action.payload;
const existingItem = state.items.find(
item => item.id === product.id
);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
}
return {
...state,
items: [
...state.items,
{ ...product, quantity: 1 }
]
};
}
case 'removeItem':
return {
...state,
items: state.items.filter(
item => item.id !== action.payload
)
};
case 'increaseQuantity':
return {
...state,
items: state.items.map(item =>
item.id === action.payload
? { ...item, quantity: item.quantity + 1 }
: item
)
};
case 'decreaseQuantity':
return {
...state,
items: state.items
.map(item =>
item.id === action.payload
? { ...item, quantity: item.quantity - 1 }
: item
)
.filter(item => item.quantity > 0)
};
case 'clearCart':
return {
...state,
items: []
};
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
18.4 Calculate the Total Price
const totalPrice = state.items.reduce(
(total, item) => total + item.price * item.quantity,
0
);
The total price is derived from the current cart items, so it does not need its own state variable. This avoids keeping two separate values synchronized.
18.5 Example Product Data
const products = [
{ id: 'p1', name: 'Notebook', price: 100 },
{ id: 'p2', name: 'Pen Set', price: 50 },
{ id: 'p3', name: 'Backpack', price: 800 }
];
18.6 Project Requirements
- Display a list of products.
- Add a product to the cart.
- Increase and decrease product quantities.
- Remove a product from the cart.
- Clear all cart items.
- Calculate the total price from the current items.
- Display an empty-cart message when there are no products.
- Prevent invalid product quantities and prices.
For a real commerce application, the server should validate prices and inventory. Client-side cart calculations are for display and must not be trusted for final payment amounts.
19. Exercises and Practice Tasks
- Create a counter using
useState with increment, decrement, and reset buttons.
- Build a profile form using object state and immutable updates.
- Create a to-do list using
useState with add and delete functions.
- Refactor the to-do list to use
useReducer.
- Add a completed status to each task and toggle it using a reducer action.
- Build a login form with email, password, and loading state.
- Create a reducer for API loading, success, and error states.
- Build a shopping cart using reducer actions.
- Calculate a derived value without storing redundant state.
- Write tests for a reducer's add, update, delete, and reset actions.
20. Interview Questions and Answers
Q1. What is state in React Native?
State is data maintained by a component between renders. When it changes, React can render the component again to display the updated interface.
Q2. What is useState?
useState is a React Hook that creates a state variable and provides a setter function for updating it.
Q3. What does useState return?
It returns an array containing the current state value and the state setter function.
Q4. What is useReducer?
useReducer is a React Hook that manages state through a reducer function and dispatched actions.
Q5. What is a reducer function?
A reducer receives the current state and an action and returns the next state.
Q6. What is dispatch?
dispatch sends an action to the reducer so it can calculate the next state.
Q7. What is an action in useReducer?
An action describes a requested change, usually through a type property and optional payload.
Q8. What is the difference between useState and useReducer?
useState uses a setter to update a state value, while useReducer uses actions and a reducer to organize state transitions.
Q9. When should you use useState?
Use it when state updates are simple, independent, and easy to understand.
Q10. When should you use useReducer?
Use it when several related updates or complex state transitions benefit from centralized logic.
Q11. Can useState manage objects?
Yes. It can manage objects, but updates should create a new object and preserve any properties that need to remain unchanged.
Q12. Why should state not be mutated?
Mutation changes existing references and can interfere with React's state update behavior. Immutable updates make changes predictable.
Q13. What is a functional state update?
It passes a function to a setter, such as setCount(previous => previous + 1), so the next value is calculated from the previous state.
Q14. Should a reducer perform API calls?
No. Reducers should be pure functions. Perform API requests in event handlers, effects, or an appropriate data-fetching layer, then dispatch the result.
Q15. Can useState and useReducer be used in the same component?
Yes. Each Hook can manage a different part of the component's state.
Q16. What is an initial state?
It is the starting value used when a state Hook initializes for a component.
Q17. How do you update an array in state?
Use immutable operations such as spread syntax, map(), and filter() to return a new array.
Q18. How can reducer logic be tested?
Call the reducer with a known initial state and action, then verify that it returns the expected next state.
Q19. What is derived state?
Derived state is a value calculated from existing state or props, such as a shopping cart total. If it can be calculated during rendering, storing it separately may be unnecessary.
Q20. Do useState and useReducer replace every state-management library?
No. They are built-in tools for component state. Larger applications may benefit from Context, a dedicated state library, or a server-state library when their sharing, caching, or synchronization needs require it.
21. Quick Revision Checklist
useState stores and updates component state.
useReducer manages state through a reducer and actions.
- A state setter requests an update; it does not mutate state directly.
- Use functional state updates when the next value depends on the previous value.
- Objects and arrays should be updated immutably.
- Reducers should be pure and return a valid next state.
- Dispatch actions to describe state transitions.
- Use
useState for simple state and consider useReducer for complex related updates.
- API requests and other side effects should happen outside reducers.
- Calculate derived values when practical instead of duplicating them in state.
- Both Hooks can be used together in one component.
22. Conclusion
Managing state is fundamental to building interactive React Native applications. The useState Hook is ideal for simple values, form fields, counters, and visibility controls. The useReducer Hook offers a structured approach when multiple actions update related data or when state transitions become more complicated.
By understanding state initialization, functional updates, immutable data handling, reducer actions, and pure reducer functions, developers can build applications that are easier to maintain, test, and extend. The best approach is the one that keeps the application's state and update logic clear without introducing unnecessary complexity.
23. Additional Learning Resources