Learnitweb

Introduction in Redux Thunk

1. Introduction

Thunks are a standard approach for writing async logic in Redux apps, and are commonly used for data fetching. The word “thunk” is a programming term that means “a piece of code that does some delayed work”. This means we can write a function which has some logic that executes later.

According to the Redux documentation: For Redux specifically, “thunks” are a pattern of writing functions with logic inside that can interact with a Redux store’s dispatch and getState methods.

Usually, Thunks is used for asynchronous logic but can be used for synchronous logic. To use thunks, you require redux-thunk middleware to be added to the Redux store as part of its configuration.

npm in redux-thunk

A thunk function is a function that accepts two arguments: the Redux store dispatch method, and the Redux store getState method. Thunk functions are not directly called by application code. Instead, they are passed to store.dispatch().

const thunkFunction = (dispatch, getState) => {
  // logic here that can dispatch actions or read state
}

store.dispatch(thunkFunction)

A thunk function may contain any arbitrary logic and can call dispatch or getState at any time. A thunk function may contain synchronous and asynchronous logic.

We use thunk action creators to generate the thunk functions that are dispatched. A thunk action creator is a function that may have some arguments, and returns a new thunk function. The thunk typically closes over any arguments passed to the action creator, so they can be used in the logic:

// fetchTodoById is the "thunk action creator"
export function fetchTodoById(todoId) {
  // fetchTodoByIdThunk is the "thunk function"
  return async function fetchTodoByIdThunk(dispatch, getState) {
    const response = await client.get(`/fakeApi/todo/${todoId}`)
    dispatch(todosLoaded(response.todos))
  }
}

The same function can be written using the arrow functions.

export const fetchTodoById = todoId => async dispatch => {
  const response = await client.get(`/fakeApi/todo/${todoId}`)
  dispatch(todosLoaded(response.todos))
}

The thunk is dispatched by invoking the action creator, just like any other Redux action.

function TodoComponent({ todoId }) {
  const dispatch = useDispatch()

  const onFetchClicked = () => {
    // Calls the thunk action creator, and passes the thunk function to dispatch
    dispatch(fetchTodoById(todoId))
  }
}

2. Need for Thunks

Real world applications require logic that have side effects. As discussed in earlier tutorials, Redux reducers must not contain side effects. Thunks give us a place to put those side effects.
It’s common practice to include logic directly within components, such as making an asynchronous request in a click handler or a useEffect hook and then processing the results. However, it’s often beneficial to move as much of that logic as possible outside the UI layer. This can enhance the testability of the logic, ensure the UI layer remains thin and purely “presentational,” and improve code reuse and sharing.

You may think that you can make your function as async but this is not possible. For example, if you try the following code, you’ll get error.

// This is not allowed
export const addTask = async (task) => {
  return { type: actionTypes.ADD_TASK, payload: { task: task } };
}; 
Error: Actions must be plain objects. Instead, the actual type was: 'Promise'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions.

3. Example

Let us see an example of Redux-thunk. We’ll modify our earlier example to demonstrate redux-thunk.

First, install the redux-thunk dependency.

npm i redux-thunk

Next step is to add redux-thunk middleware to the Redux store.

import { legacy_createStore as createStore, applyMiddleware } from "redux";
import { thunk } from "redux-thunk";

import reducer from "./reducer";

const store = createStore(reducer, applyMiddleware(thunk));
export default store;

Next, add a fetchTodo action.

import * as actionTypes from "./actionTypes";

export const addTask = (task) => {
  return { type: actionTypes.ADD_TASK, payload: { task: task } };
};

export const removeTask = (id) => {
  return { type: actionTypes.REMOVE_TASK, payload: { id: id } };
};

export const completedTask = (id) => {
  return { type: actionTypes.TASK_COMPLETED, payload: { id: id } };
};

export const fetchTodo = () => {
  return async function (dispatch, getState) {
    const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
    const task = await response.json();
    dispatch(addTask(task.title));
  };
};

actionTypes.js

export const ADD_TASK = "ADD_TASK";
export const REMOVE_TASK = "REMOVE_TASK";
export const TASK_COMPLETED = "TASK_COMPLETED";

Next, to test the change dispatch like other action to the store.

import store from "./store";
import { addTask, removeTask, completedTask, fetchTodo } from "./action";

store.subscribe(() => {
  console.log(store.getState());
});
store.dispatch(addTask("Task 1"));
store.dispatch(removeTask(1));
store.dispatch(fetchTodo());

For reference, here is the code of reducer.js

import * as actionTypes from "./actionTypes";
let id = 0;

export default function reducer(state = [], action) {
  switch (action.type) {
    case actionTypes.ADD_TASK:
      return [
        ...state,
        {
          id: ++id,
          task: action.payload.task,
          completed: false,
        },
      ];
    case actionTypes.REMOVE_TASK:
      return state.filter((task) => task.id != action.payload.id);
    case actionTypes.TASK_COMPLETED:
      return state.map((task) =>
        task.id === action.payload.id
          ? {
              ...task,
              completed: true,
            }
          : task
      );
    default:
      return state;
  }
}

4. Conclusion

In summary, Redux Thunk enhances Redux by allowing you to manage asynchronous operations seamlessly. It’s a powerful tool for building robust and responsive applications.