Handling State Management in React Native Using Redux
In this tutorial, you will manage the state of a React app using Redux. Redux helps you track and manage the state of an entire application in a single object instead of having the state and logic in a top-level component.
You will build a to-do app that centralizes the state and logic using Redux.
By the end of the tutorials you will know:
The tutorial will be in two sections. The first section explains key concepts, the Redux architecture, and the basic usage of Redux. In the next section, we will build a Todo app using Redux for state management.
Prerequisite
To get the most out of this tutorial you should be familiar with:
Introduction to State Management
React enables developers to build complex user interfaces easily. To add interactivity to the UI, React components need access to data. The data can be a response from an API endpoint or defined within the app. This data will be updated in response to an interaction, such as when a user clicks on a button, or types into an input field.
Inside a React component, the data is stored in an object called state. Whenever state changes, the component will re-render, and React will update the screen to display the new data as part of the UI.
In a React app, multiple components may need access to the state. Hence, it needs to be effectively managed. Effective state management entails being able to store and update data in an application.
What is Redux?
Redux is a pattern and library for managing and updating application state, using events called "actions". It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.
With Redux, you have a central store to keep, update, and monitor the state of your application. That means, our components may not have states. The state will be in a central location and can be accessed by multiple components in your application.
What problem does Redux solve?
A basic React app can be segmented into the following:
Every component in an app can have a state. However, it becomes a challenge if multiple components need access to the same data. To solve this issue, we "lift the state up". Lifting state up is a process where you move the state from a child component to its parent (top-level) component. With this approach, you can easily share state between multiple child components.
However, there are disadvantages to "lifting state up":
In a large-scale single-page application, our code will manage more states. This state can include server responses and cached data, as well as locally created data that has not yet been persisted to the server. It will get to the stage where you will lose control over when, why and how the state updates. Making it difficult to reproduce bugs or add new features.
A better approach is to extract the shared states from the parent component and put them into a centralized location outside the component tree.
This is a better approach because:
That is the concept behind Redux. It helps developers manage global state (a state that is needed across many parts of our app), and make the state accessible by all components irrespective of how deeply nested they are.
The first rule of Redux is that everything that can be modified in your application should be represented in a single object called the state or the state tree.
There are key terms to know when using Redux. To make it easy to understand these terms we will first consider an analogy for Redux. After this, we will define the terms in the next sections.
Redux analogy
Imagine you are the Managing Partner of a huge restaurant. To be well versed in managing the restaurant, you decide to keep track of the state of the restaurant.
You might want to track:
To keep all this information in your brain will be a hassle. Instead, you keep them in a central location called the store (redux store).
You hire an attendant (reducer) who is the only person who can update the store's information.
There are shareholders (components) who rely on the state(data) of the restaurant to update their portfolio (UI). These shareholders can only access data and cannot modify it.
Now let us assume the shareholders hire a new chef and the store's data need to be updated. Because they cannot update the data, a shareholder can send (dispatch) a note with that new information to the attendant(reducer). He then updates the previous data in the store with the latest information.
Anytime data is updated, the rest of the shareholders are notified (subscribers), and they can update their portfolio (UI)
Understanding the Redux Terminologies
Below are the new terms to be familiar with:
Actions
The second rule of Redux is that the state is read-only. You can only modify the state tree by sending an action. This ensures that neither the views nor the network callbacks will ever write directly to the state. Instead, they express an intent to transform the state.
In other words, action is the only recommended way to change the application state.
An action describes what has occurred in the application. It is a JavaScript object passed as a parameter to the store and holds the information required for the store to update the state.
An action varies in any given application. For instance, in a counter app, you will only need the following actions:
In a todo app, you may have the following actions:
With Redux, because we are separating the state from the components, the components don't know exactly how the state changes. All they care about is that they need to send an action.
The action object has a type property where you specify what event occurred in your component. That means whenever an event occurs, the event handler function will dispatch an action with what has occurred to help update the state in the store.
The action object also has a payload property. Any new data about the event will be in the payload. For instance, when I dispatch an action of type "addedTodo", the payload can contain the new to-do item and the ID.
Below are examples of action objects:
//action 1
const addTodoAction = {
type: 'todos/todoAdded', //what happened
payload: {todoID, newTodo} //data
}
//action 2
const getOrder = {
type: 'orders/getOrderStatus', //what happened
payload: {orderId, userID} //data
}
Action Creators
The action creators are functions that return action objects. Because the action creators contains the logic that can be used in multiple instances of the application, you can pass it some parameters that can be accessed in the action objects.
Below are examples of action creators:
//example 1
function addTodo(todo){
//return action object
return {
type: 'todos/addTodo',
payload: todo // what happened
}
}
//example 2
function getOrders(orderID, userID){
//return action object
return {
type: 'orders/getOrderStatus',
payload: {orderId, userID} //what happened
}
}
Reducers
A reducer is a pure function that accepts the current state and an action as arguments and returns the updated state. It is called a reducer because similar to the Array.reduce() method, the Redux reducer reduces a set of actions over time into a single state.
The reducer should be a pure function. A pure function is a function that will return the same output for the same input. It does not change or modify the global state or the state of any other functions.
What this means is :
Below is the syntax of a reducer function:
const myReducer = (state, action) => newState
The logic inside the reducer function is as below:
Below is an example of a todoReducer function:
const intialTodo = [{id:1, todo:""}]
const todoReducer = (state = initialTodo, action)=>{
if(action.type === "todos/AddedTodo"){
return [...state, todo: action.payload]
}else{
return state
}
}
Below is what is happening:
The third principle of Redux is that to describe state changes, you declare a function that accepts the previous state of the app, the action being dispatched, and returns the next state of the app
Store
A store in an object that holds the entire state of your application. It is the central location of data and where data is updated.
The store has three main methods:
Below is an example of how to create a store in redux.
//store.js
//import your root reducer
import rootReducer from "./rootReducer"
//import createStore from redux
import {createStore} from "redux"
//create the store
const store =createStore(rootReducer)
Dispatch
Dispatch is used to send action to our store . It is the only way to change the state of our app.
To update the state , you will call the store.dispatch() method. When dispatch() is called, the store will execute the reducers (the reducers have access to the current state and an action as input, and perform some logic). The store then updates its state with the output of the reducers.
The store.dispatch() method accepts the action object as an argument.
store.dispatch({ type: 'todo/addedTodo' })
In the snippet above, for instance, whenever a user enters a new to-do, you will dispatch the action to the store. Because there is a reducer function inside the store, it will use the dispatched action.type to determine the logic for the new state.
Selectors
Selectors are functions that help you extract specific data from the state. It accepts the state as an argument and returns the data to retrieve from the state.
You will use the selector in your component to get specific data from the state.
const selectLatestTodo = state => state.data //selector function
const currentValue = selectLastestTodo(store.getState())
console.log(currentValue)
// Buy milk
Illustration of the Redux architechture
In this section, we will use all the terminologies learned to explain how data flow in our app, and how the UI is re-rendered when the state changes.
Let's take a look at the setup of Redux:
Updating the state:
Creating a store, subscribing and dispatching actions
In this section, we will learn how to create a store and dispatch actions to the store.
The Redux store brings together the state, reducer and action of our application. It is the central location of the application's state.
领英推荐
The functions of the store are to:
Creating a store
Use the createStore() method from the Redux library to create the store. This method accepts a reducer function as an argument.
Below is a code snippet on how to create a store:
//store.js
import { createStore} from 'redux'
const store = createStore(rootReducer)
Next, you will need to pass the "root reducer" function to the createStore(). The root reducer combines all of the other reducers in your application.
To create a root reducer, you import the combineReducer() method from the Redux library. The combineReducer helps you combine all the different reducer functions. It accepts an object of reducer functions as its argument. The key of the object will become the keys in your root state object, and the values are the reducer functions.
Below is an example of how to create a rootReducer :
import { combineReducers } from 'redux';
const reducers = {
user: userReducer,
cart: cartReducer,
orders: ordersReducer,
};
const rootReducer = combineReducers(reducers);
Now, you have learned how to
Next, you will learn how to get the initial state of the store and dispatch actions to the store
Dispatching actions to the store
To update the state of the application, the component needs to dispatch actions.
Below is how to do that:
//actions object
const addTodo = {
type: 'todos/todoAdded',
payload: "Buy milk"
});
const completeTodo = {
type: 'todos/todoRemoved',
payload: 2 // id of the todo to complete
}
//dispatch the action to update the state
store.dispatch(addTodo)
store.dispatch(completeTodo)
Subscribing to the store
Use the subscribe() method to subscribe to a store. The subscribe() method will listen for changes to the state of your app. This will help you update the UI to reflect the current state, perform side effects, or any task that needs to be done when the state changes.
In the code snippet below illustrates how to listen for updates, and log the latest state to the console:
const subscription = store.subscribe(() => {
// Do something when the state changes.
console.log('State after dispatch: ', store.getState())
});
Adding Redux to a React app
In this section, you will use the concept learned to build a to-do app with basic functionalities ( add, delete, and complete a todo) while using redux for state management.
We will not go in-depth into each implementation as we have covered the concepts earlier.
Here are the steps to follow:
The repository for the project is found here. It has branches for each major step we will implement.
Let's get started
Step 1: Setting up your project
Create a React app in your project directory by following the steps below:
Step 2: Creating the Todo components
Below is the UI for our app.
Now, let's create the required components
Step 3: Creating the store
Next, we will need to create a store to keep track of and manage the entire state of the application. We do that using the createStore() method from Redux.
Follow these steps:
//store/store.js
import { createStore } from "redux";
import todoReducer from "../reducer/todoReducer";
const store = createStore(todoReducer);
export default store;
Step 4: Defining the reducer function
We will define our reducer inside a todoReducer.js file. Reducers are functions that contain the logic required to update the state and return a new state. The todoReducer.js will also contain the initial state of our app.
Follow the steps below to define a reducer function:
Add the code snippet below to the file
//state object
const initialState = {
todos: [
{
id: 1,
item: "Learn redux fundamentals",
completed: false,
},
{
id: 2,
item: "Build a todo-app",
completed: false,
},
],
};
//define the reducer logic
const todoReducer = (state = initialState, action) => {
switch (action.type) {
//logic to add a new todo
case "todos/addedTodo":
return {
...state,
todos: [...state.todos, action.payload],
};
//logic to delete a todo
case "todos/deleteTodo":
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload),
};
// logic to complete a todo
case "todos/completeTodo":
return {
...state,
todos: state.todos.map((todo) => {
if (todo.id === action.payload) {
return {
...todo,
completed: !todo.completed,
};
} else {
return todo;
}
}),
};
default:
return state;
}
};
export default todoReducer;
In the code above:
Step 5: Wrapping the Provider component around your app
The Provider component enables the Redux store to be available to any nested components that need to access the store.
Since any React component in a React Redux app can be connected to the store, most applications will render a Provider at the top level, with the entire app’s component tree inside of it.
Below is how to wrap our root component inside a Provider
//main,jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { Provider } from "react-redux"; //Provider
import store from "./store/store.js"; //store
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
In the code above, we have wrapped the <Provider/> around <App/> component to enable all nested components to access the store
Next, we would read and display data from the store using the useSelector hook
Step 6: Reading and displaying the todos with useSelector hook
React-Redux has its custom hooks, which you can use in your components. The useSelector hook lets the React components read data from the Redux store. It accepts a selector function that takes the entire Redux store state as its argument, reads some value from the state, and returns that result.
Follow the steps below to read and display data:
The code below illustrates how to read the todos from our store.
//components/TodoList.jsx
import React from "react";
import { useSelector } from "react-redux";
import TodoItem from "./TodoItem";
const TodoList = () => {
//callback function
const selectTodos = (state) => state.todos;
//extract the todos
const returnedTodos = useSelector(selectTodos);
const displayTodos = returnedTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
));
return <div>{displayTodos}</div>;
};
export default TodoList;
Let's understand the code above:
We know how to read and display data from the store. Next, we will learn how to dispatch actions from the components to update the store
Step 5: Dispatching actions with useDispatch hook
The useDispatch hook provides access to the dispatch method that is needed to dispatch actions to update the state.
We can call const dispatch = useDispatch() in any component that needs to dispatch actions, and then call dispatch(someAction) as needed.
In the TodoInput component, let's dispatch an action to add a new todo:
In the code above, on submitting a new todo:
Because we have imported the useSelector hook, we can easily add a new todo to the store's state, and it will reflect in the UI.
Below is what we have done so far
Dispatching action on clicking the "delete" and "complete" buttons
In the TodoItem components, we can now click on the "delete" and "complete" button. On clicking these buttons we dispatch actions to delete and complete a todo. These are handled in the onDelete and onComplete action creators.
The code snippet is as below:
//components/TodoItem.jsx
import { useDispatch } from "react-redux";
const TodoItem = ({ todo }) => {
const dispatch = useDispatch();
//delete a todo
const onDelete = (id) => {
return dispatch({
type: "todos/deleteTodo",
payload: id,
});
};
//complete Todo
const onComplete = (id) => {
return dispatch({
type: "todos/completeTodo",
payload: id,
});
};
return (
<div>
<h3 className={`todo${todo.completed ? "Completed" : ""}`}>
{todo.item}
</h3>
<div>
<button onClick={() => onComplete(todo.id)}>Complete</button>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
</div>
);
};
export default TodoItem;
Redux vs Context API
The difference between Redux and Context API is how they manage states. In Redux state is managed in a central store. However, the Context API deals with state updates on a component level, as they happen within each component.
You might ask, can't you use the useContext hook from the Context API to pass state to multiple components since that eliminates prop drilling?
In a scenario where a state affects multiple components or is required by all the components in an app, you can use the useContext hook to manage state. This avoids props drilling and makes data easily accessible in all components.
However, there are some disadvantages to using useContext:
Below are some scenarios you might use Redux over useContext:
Conclusion
In this tutorial, you managed the state of a React To do app using Redux. Next, learn how to manage the state using the Redux Toolkit. Redux Toolkit makes it easier to write good Redux applications and speeds up development. Furthermore, learn Redux DevTools to help you trace when, where, why, and how your application's state changed.