Module: Redux Toolkit

Async Logic with Thunks

React JS: Redux Toolkit - Async Logic with Thunks

This document outlines how to handle asynchronous logic (like API calls) within your Redux application using Redux Toolkit and Thunks.

Why Thunks?

Redux actions, by default, must be synchronous. This means you can’t directly perform asynchronous operations like fetching data from an API within an action creator. Thunks solve this problem by:

  • Allowing asynchronous logic:Thunks let you write action creators that return afunctioninstead of an action object. This function can then perform asynchronous operations.
  • Dispatching actions:Inside the thunk function, you can dispatch standard Redux actions when the asynchronous operation completes (or fails).
  • Access todispatchandgetState:Thunk functions receivedispatchandgetStateas arguments, giving them full access to the Redux store.

Setting up Redux Toolkit

First, ensure you have Redux Toolkit installed:


npm install @reduxjs/toolkit

Creating a Slice with Async Thunk

Let's create a slice to manage a list of posts fetched from an API.

// features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// Define an async thunk for fetching posts
export const fetchPosts = createAsyncThunk(
  'posts/fetchPosts', // Action type string
  async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts');
    const data = await response.json();
    return data; // This becomes the `payload` of the fulfilled action
  }
);

const postsSlice = createSlice({
  name: 'posts',
  initialState: {
    posts: [],
    loading: 'idle', // 'idle' | 'pending' | 'succeeded' | 'failed'
    error: null,
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchPosts.pending, (state) => {
        state.loading = 'pending';
        state.error = null;
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.loading = 'succeeded';
        state.posts = action.payload;
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.loading = 'failed';
        state.error = action.error.message;
      });
  },
});

export const { } = postsSlice.actions; // No regular actions needed in this example
export default postsSlice.reducer;

Explanation:

  • createAsyncThunk: This function creates an asynchronous thunk.
    • The first argument is a unique action type string (e.g., 'posts/fetchPosts'). This is used to identify the different states of the async operation.
    • The second argument is an async function that performs the asynchronous operation (in this case, fetching data). The returned value from this function becomes the payload of the fulfilled action.
  • extraReducers: This allows you to define how your slice's state should change based on the different states of the async thunk.
    • fetchPosts.pending: Handles the state when the thunk is in progress (e.g., setting loading to 'pending').
    • fetchPosts.fulfilled: Handles the state when the thunk successfully completes (e.g., setting posts to the fetched data and loading to 'succeeded'). action.payload contains the data returned from the async function in createAsyncThunk.
    • fetchPosts.rejected: Handles the state when the thunk fails (e.g., setting error to the error message and loading to 'failed'). action.error contains the error object.

Dispatching the Thunk

In your component, you can dispatch the thunk using dispatch:

// components/PostsList.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchPosts } from '../features/posts/postsSlice';

function PostsList() {
  const dispatch = useDispatch();
  const posts = useSelector((state) => state.posts.posts);
  const loading = useSelector((state) => state.posts.loading);
  const error = useSelector((state) => state.posts.error);

  useEffect(() => {
    dispatch(fetchPosts());
  }, [dispatch]);

  if (loading === 'pending') {
    return <p>Loading posts...</p>;
  }

  if (error) {
    return <p>Error fetching posts: {error}</p>;
  }

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

export default PostsList;

Explanation:

  • useDispatch: Gets the dispatch function from the Redux store.
  • useSelector: Gets the posts, loading, and error values from the Redux store.
  • useEffect: Dispatches the fetchPosts thunk when the component mounts.
  • Conditional Rendering: Displays loading, error, or post list based on the loading and error states.

Handling Multiple Async Operations

You can create multiple async thunks within a single slice or across different slices. Each thunk will have its own pending, fulfilled, and rejected action types.

Thunks with Arguments

Sometimes you need to pass arguments to your thunk. You can do this by returning a function from the action creator:

// features/posts/postsSlice.js
export const fetchPostById = createAsyncThunk(
  'posts/fetchPostById',
  async (postId) => { // Receive postId as argument
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
    const data = await response.json();
    return data;
  }
);

// In your component:
dispatch(fetchPostById(1)); // Pass the postId as an argument

Best Practices

  • Error Handling: Always handle errors in your rejected case. Display user-friendly error messages.
  • Loading State: Provide visual feedback to the user while data is loading.
  • Action Type Naming: Use a consistent naming convention for your action types (e.g., feature/actionName).
  • Keep Thunks Focused: Each thunk should ideally handle a single, specific asynchronous operation.
  • Use createAsyncThunk: It simplifies the handling of pending, fulfilled, and rejected states.

This provides a solid foundation for handling asynchronous logic in your Redux Toolkit applications. Remember to adapt these examples to your specific needs and API endpoints.