DEV Community

Vishnu Singh
Vishnu Singh

Posted on

Redux saga v/s redux thunk or both?

Note: If you have not used redux-saga then you might not understand the problem we are going to discuss.

Introduction

Welcome to the world of Redux middleware! If you've been exploring articles discussing the comparisons between redux-saga and redux-thunk, you've likely encountered various perspectives. While redux-saga offers unique advantages, personally, I find the simplicity of redux-thunk quite appealing.

I have experimented with both approaches individually, and I have come to the conclusion that utilizing sagas provides a greater degree of control and capability. However, there is one crucial element lacking in sagas, which is the ability to await actions like thunks. There are instances where it is necessary to be aware of when a saga execution has finished or encountered an error. For example, you may want to conceal a loading indicator at the appropriate time.

I have developed a compact Redux middleware named redux-thaga, which combines the functionalities of both redux-thunk and redux-saga. The name may appear lighthearted, as it is a fusion of "thunk" and "saga," resulting in "redux-thaga."

Usage

We can create a thaga action as follows:

// create the action
export const fetchTasks = createThagaAction(
  'fetchTasks',
  function* fetchTasksWorker({ status }) { // arguments: (actionPayload, action, ...restArgs)
    const tasks = (yield call(taskApi, status)) as Task[];
    return tasks;
  }
);

function* rootSaga() {
  yield takeLatest(fetchTasks, fetchTasks.worker);
}

// I am using react but you can use it with any framework or library
const TaskList = () => {
  const dispatch = useDispatch();

  const { isFetchingTasks, setIsFetchingTasks } = useState(false);
  const { tasks, setTasks } = useState([]);

  const fetchTasks = async (status: 'pending' | 'completed') => {
    try {
      setIsFetchingTasks(true);
      // you can await following action like thunks! 🎉
      const tasks = await dispatch(fetchTasks({ status }));
      setTasks(tasks);
    } catch (error) {
      console.log(error);
    } finally {
      setIsFetchingTasks(false);
    }
  }

  const fetchPendingTasks = () => fetchTasks('pending');
  const fetchCompletedTasks = () => fetchTasks('completed');

  return (
    <div>
      <button onClick={fetchPendingTasks}>Fetch pending tasks</button>
      <button onClick={fetchCompletedTasks}>Fetch completed tasks</button>
      {isFetchingTasks && <div>Fetching tasks...</div>}
      {tasks.map(task => <div key={task.id}>{task.title}</div>)}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The fetchTasks action will have three associated actions along with a saga as its properties:

  1. worker - saga
  2. finished - action
  3. failed - action
  4. cancelled - action

When worker is successfully executed, the finished action is automatically dispatched. Similarly if the worker encountered an unhandled error then failed action will be dispatched.

The cancelled is a special action which is dispatched only when the saga is cancelled like how takeLatest does for previously uncompleted saga.

Now you may think what is the point of converting a saga into a thunk, we could have used both redux-thunk along with redux-saga!

Yes we can, only the problem is we can't fork or do some saga thing inside the body of a thunk as it's not a generator function. We have to dispatch another action for it.

Please note that I am not suggesting to create all the sagas this way, but you can use it when you need await-ability along with saga features.

You can see the source code here on github. Please consider giving it a star if you like it.

Lastly, I would greatly appreciate and warmly welcome any suggestions you might have.

Cheers 👋

Top comments (0)