A Beginner’s Guide to Redux: Step by Step Explanation with Examples
Introduction
Hello there! 🌟 If you're new to web development, you might have heard about React and Redux. Maybe you're wondering, "What exactly are these, and why do I need them?" Don't worry—I'm here to guide you through everything from the basics.
Let’s start from scratch and understand why Redux is important and how to use it. We'll also cover how to set up a simple Redux application, even if you haven't worked with React before. Ready? Let’s dive in!
What is Redux?
Imagine you're building a big app, like a social media platform. As your app grows, you need a way to keep track of all the information (like user data, posts, comments) in one organized place. That's where **Redux** comes in.
Redux is like a giant container where you store all the information (or "state") of your app. This makes it easier to manage, especially as your app becomes more complex. Redux helps you keep everything in sync, so different parts of your app can share and update information without getting messy.
Why Do We Need Redux?
In a small app, you might not need Redux. But as your app grows, you might find it hard to keep track of what's going on. For example:
- How do you make sure all parts of your app know when a user logs in?
- How do you update the app when new data arrives without causing bugs?
Redux solves these problems by providing a structured way to manage your app’s data (or "state").
The Three Key Concepts of Redux
1. Actions: Think of actions as “messages” that you send to Redux to tell it that something happened. For example, “A user just logged in” or “A new post was added.”
2. Reducers: Reducers are functions that tell Redux how to update the app's state based on the action it received. For example, when a user logs in, the reducer will update the state to store that user's information.
3. Store: The store is where all the app's state lives. It’s the big box that holds everything together.
Let’s Build a Simple Example
Imagine you're building a simple to-do list app where users can add tasks. We’ll use Redux to manage this app’s state.
If you have already installed react and necessary library, skip step 1
Step 1: Set Up Your Project
First, let's create a new project. You can use a tool called Create React App to set up everything you need:
npx create-react-app my-redux-app
Copy the above command and paste into you vs-code terminal or command prompt and hit [Enter]. Press Y and enter to begin he installation
Once the installation is finished, you will see the success message in terminal
Navigate to you project >> cd my-redux-app
Paste below command to install the necessary package
npm install redux react-redux
This command will create a new React project and install Redux and React-Redux, which helps React work with Redux.
Your folder structure will should look like this
Step 2: Create Actions
Let’s start by creating some actions. Remember, actions are like messages you send to Redux. For our to-do app, we might need actions like “Add a new task” and “Delete a task.”
Create a new folder called actions
inside the src
directory and add a file named index.js
:
// src/actions/index.js
export const ADD_TODO = 'ADD_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const addTodo = (todo) => ({
type: ADD_TODO,
payload: todo
});
export const deleteTodo = (index) => ({
type: DELETE_TODO,
payload: index
});
Explanation:
ADD_TODO
andDELETE_TODO
are action types. They describe the kind of action we want to perform.addTodo
anddeleteTodo
are action creators. These are functions that return the actual action objects.
Step 3: Create Reducers
Next, we need to create a reducer. A reducer will decide how our state should change when an action is dispatched (or sent) to Redux.
Create a new folder called reducers
inside src
and add a file named todoReducer.js
:
// src/reducers/todoReducer.js
import { ADD_TODO, DELETE_TODO } from '../actions';
const initialState = {
todos: []
};
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload]
};
case DELETE_TODO:
const newTodos = state.todos.filter((_, index) => index !== action.payload);
return {
...state,
todos: newTodos
};
default:
return state;
}
};
export default todoReducer;
Explanation:
initialState
: This is the starting point of our app’s state. We begin with an empty list of todos.- The
todoReducer
function checks the type of action (ADD_TODO
orDELETE_TODO
) and decides how to update the state.
Step 4: Combine Reducers (If Needed)
If you have more than one reducer (e.g., one for todos, one for user login), you need to combine them. For now, since we only have one reducer, we’ll just create a root reducer.
Create a file named index.js
inside the reducers
folder:
// src/reducers/index.js
import { combineReducers } from 'redux';
import todoReducer from './todoReducer';
const rootReducer = combineReducers({
todos: todoReducer
});
export default rootReducer;
Explanation: combineReducers
is a function that lets us combine multiple reducers into one. Even though we only have one reducer now, it's a good habit to use combineReducers
for scalability.
Step 5: Create the Store
Now we need to create the Redux store, where all the state lives. The store will use our reducer to manage the state.
Create a new folder called store
inside src
and add a file named index.js
:
// src/store/index.js
import { createStore } from 'redux';
import rootReducer from '../reducers';
const store = createStore(rootReducer);
export default store;
Explanation: createStore
: This function creates the Redux store using our root reducer.
Step 6: Provide the Store to Your App
To make Redux available in our app, we need to wrap our main component with a Provider
. This makes the Redux store available to any component in our app.
Edit the src/index.js
file:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
Explanation:
The Provider
component wraps our entire app and gives it access to the Redux store.
Step 7: Connect Your Components to Redux
Now that everything is set up, let's connect our React components to Redux.
Example 1: Displaying the To-Do List
Create a new file called TodoList.js
inside the src/components
folder:
// src/components/TodoList.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { deleteTodo } from '../actions';
const TodoList = () => {
const todos = useSelector((state) => state.todos.todos);
const dispatch = useDispatch();
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>
{todo} <button onClick={() => dispatch(deleteTodo(index))}>Delete</button>
</li>
))}
</ul>
);
};
export default TodoList;
Explanation: useSelector
: This hook lets you access the Redux state. Here, we use it to get the list of todos.useDispatch
: This hook gives you the dispatch
function, which you can use to send actions to Redux.
Example 2: Adding a New To-Do
Create a new file called AddTodo.js
inside the src/components
folder:
// src/components/AddTodo.js
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from '../actions';
const AddTodo = () => {
const [input, setInput] = useState('');
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (input.trim()) {
dispatch(addTodo(input));
setInput('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit">Add Todo</button>
</form>
);
};
export default AddTodo;
Explanation: useState
: This React hook is used to manage the input state.handleSubmit
: This function adds a new to-do when the form is submitted.
Step 8: Putting It All Together
Finally, let's put it all together in the `App.js` file:
// src/App.js
import React from 'react';
import TodoList from './components/TodoList';
import AddTodo from './components/AddTodo';
const App = () => (
<div>
<h1>My To-Do List</h1>
<AddTodo />
<TodoList />
</div>
);
Explanation:
This is our main component that displays the header, the input form for adding a new to-do, and the list of existing to-dos.
Let's outline the folder structure for the Redux-based application we've discussed. This structure will help you keep your files organized as your project grows.
Folder Structure Overview
my-redux-app/
├── node_modules/
├── public/
│ ├── index.html
│ └── favicon.ico
├── src/
│ ├── actions/
│ │ └── index.js
│ ├── components/
│ │ ├── AddTodo.js
│ │ └── TodoList.js
│ ├── reducers/
│ │ ├── index.js
│ │ └── todoReducer.js
│ ├── store/
│ │ └── index.js
│ ├── App.js
│ ├── index.js
│ └── index.css
├── .gitignore
├── package.json
└── README.md
Detailed Explanation of Each Folder and File
node_modules/
:- This folder contains all the dependencies (packages) installed via npm. You typically don't need to touch this folder directly.
public/
:index.html
: The main HTML file. It serves as the entry point for your React application. The rootdiv
here is where your entire React app gets rendered.favicon.ico
: The icon that appears in the browser tab for your site.
src/
: This is the main directory where all your source code lives.actions/
:index.js
: This file contains all the action types and action creators. Actions describe events that occur in the application and are dispatched to update the state.
components/
:AddTodo.js
: A React component that handles adding new to-dos. It includes a form and the logic to dispatch the action to add a new to-do.TodoList.js
: A React component that displays the list of to-dos. It usesuseSelector
to access the to-dos from the Redux store anduseDispatch
to delete a to-do.
reducers/
:index.js
: The root reducer, where you combine multiple reducers (if you have more than one) into a single reducer.todoReducer.js
: This reducer handles the state for the to-do list, including adding and deleting to-dos and managing the state during API fetch operations.
store/
:index.js
: This file sets up the Redux store using the root reducer and applies any middleware (likeredux-thunk
).
App.js
:- The main React component where the application is composed. It includes the main structure of the app, bringing together different components like
TodoList
andAddTodo
.
- The main React component where the application is composed. It includes the main structure of the app, bringing together different components like
index.js
:- The entry point for the React application. It renders the
App
component into theindex.html
file's rootdiv
. This file also includes theProvider
component fromreact-redux
to connect your React app with Redux.
- The entry point for the React application. It renders the
index.css
:- The global CSS file where you can include styles for your entire application.
.gitignore
:- A file that tells Git which files and directories to ignore in version control. Typically, you’ll ignore things like
node_modules/
, build files, and environment variables.
- A file that tells Git which files and directories to ignore in version control. Typically, you’ll ignore things like
package.json
:- This file contains metadata about your project, including dependencies, scripts, and other configurations.
README.md
:- A markdown file that serves as the main documentation for your project. You can describe your app, how to run it, and any other important information here.
Adding Middleware to Your Redux Application
Middleware in Redux is like a translator that sits between the action and the reducer. It can intercept actions before they reach the reducer, allowing you to modify them, log them, or even stop them entirely. Middleware is essential for handling asynchronous actions, like fetching data from an API.
Let’s dive into how middleware works in Redux and how you can use it to enhance your app.
What is Middleware?
Middleware is a piece of code that runs between dispatching an action and the moment it reaches the reducer. It’s like an extra layer of functionality that you can add to your Redux flow.
Common use cases for middleware include:
- Logging: Track every action dispatched and the resulting state.
- Asynchronous Actions: Handle things like API calls where you need to dispatch actions after receiving data.
- Conditional Dispatch: Stop or modify actions based on certain conditions.
How to Add Middleware in Redux
Redux comes with a few built-in middlewares like redux-thunk
and redux-saga
. Let’s focus on redux-thunk
, as it’s simpler and commonly used.
Step 1: Install Redux Thunk
First, install the redux-thunk
package: npm install redux-thunk
Step 2: Apply Middleware to Your Store
To use middleware, you need to apply it when you create the Redux store. Let’s modify our store/index.js
file to include redux-thunk
:
// src/store/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
export default store;
Explanation:
applyMiddleware(thunk)
: This function adds the thunk middleware to the Redux store, enabling you to handle asynchronous actions.
Using Middleware for Asynchronous Actions
Now that we have middleware set up, let's see it in action by fetching data from an API.
Step 3: Create Asynchronous Action Creators
Let’s modify our to-do app to fetch initial tasks from an API when the app loads.
First, we’ll create a new action type and an asynchronous action creator to fetch the data:
// src/actions/index.js
export const FETCH_TODOS_REQUEST = 'FETCH_TODOS_REQUEST';
export const FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS';
export const FETCH_TODOS_FAILURE = 'FETCH_TODOS_FAILURE';
export const fetchTodos = () => {
return async (dispatch) => {
dispatch({ type: FETCH_TODOS_REQUEST });
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
const data = await response.json();
dispatch({ type: FETCH_TODOS_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: FETCH_TODOS_FAILURE, payload: error.message });
}
};
};
Explanation:
FETCH_TODOS_REQUEST
,FETCH_TODOS_SUCCESS
,FETCH_TODOS_FAILURE
: These action types represent the different states of an API call (starting, success, failure).fetchTodos
: This action creator usesredux-thunk
to perform an asynchronous operation. It first dispatches a request action, then attempts to fetch data from the API, and finally dispatches either a success or failure action depending on the result.
Step 4: Update the Reducer to Handle New Actions
Next, we need to update our reducer to handle these new actions:
// src/reducers/todoReducer.js
import { ADD_TODO, DELETE_TODO, FETCH_TODOS_REQUEST, FETCH_TODOS_SUCCESS, FETCH_TODOS_FAILURE } from '../actions';
const initialState = {
todos: [],
loading: false,
error: null
};
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload]
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter((_, index) => index !== action.payload)
};
case FETCH_TODOS_REQUEST:
return {
...state,
loading: true,
error: null
};
case FETCH_TODOS_SUCCESS:
return {
...state,
loading: false,
todos: action.payload
};
case FETCH_TODOS_FAILURE:
return {
...state,
loading: false,
error: action.payload
};
default:
return state;
}
};
export default todoReducer;
Explanation:
- The reducer now handles additional actions related to the asynchronous fetch operation, updating the state to show loading, success, or error.
Step 5: Dispatch Asynchronous Action in a Component
Finally, let’s trigger the fetchTodos
action when our app loads:
// src/App.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTodos } from './actions';
import TodoList from './components/TodoList';
import AddTodo from './components/AddTodo';
const App = () => {
const dispatch = useDispatch();
const loading = useSelector((state) => state.todos.loading);
const error = useSelector((state) => state.todos.error);
useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
return (
<div>
<h1>My To-Do List</h1>
{loading && <p>Loading...</p>}
{error && <p>Error: {error}</p>}
<AddTodo />
<TodoList />
</div>
);
};
export default App;
Explanation:
useEffect
: This React hook is used to trigger thefetchTodos
action as soon as the component mounts.- The app now displays a loading message while the data is being fetched and shows an error message if something goes wrong.
Conclusion
Congratulations! 🎉 You've just built a simple to-do app using Redux. Even if this is your first time, you've managed to:
- Understand the core concepts of Redux (actions, reducers, store).
- Set up a Redux store and connect it to a React app.
- Use Redux to manage the state of a simple to-do list.
As you continue to explore, you'll discover more advanced features and use cases. But for now, you've taken a big step towards mastering Redux!