Introduction
Redux Saga is a middleware library for handling side effects in Redux applications. Side effects are operations like asynchronous data fetching, interacting with the browser, and more, which are often performed in modern web applications.
Redux Saga aims to make these side effects more manageable and testable by providing a way to handle them in a structured and predictable manner.
Benefits of Using Redux Saga:
- Separation of Concerns:
Redux Saga allows you to separate the side effect logic from the components and reducers, making your codebase more modular and maintainable. - Asynchronous Flow Control:
It provides a way to handle asynchronous operations, such as API calls, in a more readable and synchronous-like manner using generators. - Testability:
Sagas are easy to test because they are just functions, and you can assert the yielded values step by step, ensuring that the saga behaves as expected. - Cancellation:
Redux Saga provides a way to cancel asynchronous operations easily, which can be helpful in scenarios like canceling an ongoing API request when a user navigates away from a page. - Error Handling:
Sagas make it easy to handle errors in asynchronous operations by providing built-in constructs for catching and handling errors.
Now, let's create a small TODO list project with React and Redux Saga:
Install Dependencies:
npm install react react-dom redux react-redux redux-saga
actions.js:
- Defines an action creator addTodo that returns an action of type "ADD_TODO_ASYNC" with a payload containing the todo text.
export const addTodo = (text) => ({
type: "ADD_TODO_ASYNC",
payload: { text }
});
reducers.js:
- Defines the initial state for the Redux store, which includes an empty array for todos.
- Provides a todoReducer function that handles the "ADD_TODO" action. It adds a new todo to the state with a unique ID and the text from the action payload.
const initialState = {
todos: []
};
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case "ADD_TODO":
console.log("reducer", state);
return {
todos: [
...state.todos,
{ id: Date.now(), text: action.payload.text.text, completed: false }
]
};
default:
return state;
}
};
export default todoReducer;
store.js:
- Creates the Redux store using createStore from Redux and applies middleware (redux-saga middleware) using applyMiddleware.
- The store is initialized with the todoReducer.
- It creates a sagaMiddleware and runs the root saga (rootSaga) using sagaMiddleware.run(rootSaga).
import { applyMiddleware, createStore } from "redux";
import todoReducer from "./reducers";
import createSagaMiddleware from "redux-saga";
import rootSaga from "./saga";
const sagaMiddleware = createSagaMiddleware();
const store = createStore(todoReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
export default store;
saga.js:
- Defines a saga (addTodoSaga) that listens for "ADD_TODO_ASYNC" actions and dispatches a corresponding "ADD_TODO" action.
import { put, takeEvery } from "redux-saga/effects";
function* addTodoSaga(action) {
console.log(action.payload);
yield put({ type: "ADD_TODO", payload: action.payload });
}
function* rootSaga() {
yield takeEvery("ADD_TODO_ASYNC", addTodoSaga);
}
export default rootSaga;
TodoForm.js:
- This is a functional component that represents a form for adding new todos.
- It uses useState from React to manage the state of the input field.
- It utilizes the useDispatch and useSelector hooks from react-redux to interact with the Redux store.
- The form has an input field and a button to add todos.
- When the form is submitted, it dispatches the addTodo action with the entered text.
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addTodo } from "./actions";
const TodoForm = () => {
const todos = useSelector((state) => state.todos); // Assuming todoReducer is an array
console.log("Todos", todos);
const dispatch = useDispatch();
const [text, setText] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim() !== "") {
dispatch(addTodo({ text }));
setText("");
}
};
return (
<>
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add Todo</button>
</form>
<div>
{todos.length > 0 ? (
todos.map((task) => <li key={task.id}>{task.text}</li>)
) : (
<h1>No Tasks found</h1>
)}
</div>
</>
);
};
export default TodoForm;
Use Cases for Sagas:
- Handling Asynchronous Actions:
Sagas are well-suited for managing actions that involve asynchronous operations, such as fetching data from an API. - Interactions with External Services:
If your application needs to interact with external services, sagas can handle those interactions and keep your components focused on presentation. - Complex State Transitions:
When the state transitions in your application become complex and involve multiple steps, sagas can provide a more structured way to manage these transitions.
In summary, this code sets up a basic Redux store with a saga middleware to handle asynchronous actions, with a focus on adding todos to a list in response to user input. If you found this tutorial helpful and would like to explore the code further or have any questions, connect with me on GitHub. You can also experiment with the code in real-time on CodeSandbox.
If you're interested in diving deeper into core Redux concepts, check out this comprehensive Redux tutorial on my blog. It covers essential aspects to help you master Redux.
Happy coding!