How to use Redux-Thunk with Redux Toolkit's createSlice?
Asked Answered
G

4

60

I have come across Redux Toolkit (RTK) and wanting to implement further functionality it provides. My application dispatches to reducers slices created via the createSlice({}) (see createSlice api docs)

This so far works brilliantly. I can easily use the built in dispatch(action) and useSelector(selector) to dispatch the actions and receive/react to the state changes well in my components.

I would like to use an async call from axios to fetch data from the API and update the store as the request is A) started B) completed.

I have seen redux-thunk and it seems as though it is designed entirely for this purpose, but the new RTK does not seem to support it within a createSlice() following general googling.

Is the above the current state of implementing thunk with slices?

I have seen in the docs that you can add extraReducers to the slice but unsure if this means I could create more traditional reducers that use thunk and have the slice implement them?

Overall, it is misleading as the RTK docs show you can use thunk, but doesn't seem to mention it not being accessible via the new slices api.

Example from Redux Tool Kit Middleware

const store = configureStore({
  reducer: rootReducer,
  middleware: [thunk, logger]
})

My code for a slice showing where an async call would fail and some other example reducers that do work.

import { getAxiosInstance } from '../../conf/index';

export const slice = createSlice({
    name: 'bundles',
    initialState: {
        bundles: [],
        selectedBundle: null,
        page: {
            page: 0,
            totalElements: 0,
            size: 20,
            totalPages: 0
        },
        myAsyncResponse: null
    },

    reducers: {
        //Update the state with the new bundles and the Spring Page object.
        recievedBundlesFromAPI: (state, bundles) => {
            console.log('Getting bundles...');
            const springPage = bundles.payload.pageable;
            state.bundles = bundles.payload.content;
            state.page = {
                page: springPage.pageNumber,
                size: springPage.pageSize,
                totalElements: bundles.payload.totalElements,
                totalPages: bundles.payload.totalPages
            };
        },

        //The Bundle selected by the user.
        setSelectedBundle: (state, bundle) => {
            console.log(`Selected ${bundle} `);
            state.selectedBundle = bundle;
        },

        //I WANT TO USE / DO AN ASYNC FUNCTION HERE...THIS FAILS.
        myAsyncInSlice: (state) => {
            getAxiosInstance()
                .get('/')
                .then((ok) => {
                    state.myAsyncResponse = ok.data;
                })
                .catch((err) => {
                    state.myAsyncResponse = 'ERROR';
                });
        }
    }
});

export const selectBundles = (state) => state.bundles.bundles;
export const selectedBundle = (state) => state.bundles.selectBundle;
export const selectPage = (state) => state.bundles.page;
export const { recievedBundlesFromAPI, setSelectedBundle, myAsyncInSlice } = slice.actions;
export default slice.reducer;

My store setup (store config).

import { configureStore } from '@reduxjs/toolkit';
import thunk from 'redux-thunk';

import bundlesReducer from '../slices/bundles-slice';
import servicesReducer from '../slices/services-slice';
import menuReducer from '../slices/menu-slice';
import mySliceReducer from '../slices/my-slice';

const store = configureStore({
    reducer: {
        bundles: bundlesReducer,
        services: servicesReducer,
        menu: menuReducer,
        redirect: mySliceReducer
    }
});
export default store;
Graphophone answered 20/2, 2020 at 9:6 Comment(0)
D
118

I'm a Redux maintainer and creator of Redux Toolkit.

FWIW, nothing about making async calls with Redux changes with Redux Toolkit.

You'd still use an async middleware (typically redux-thunk), fetch data, and dispatch actions with the results.

As of Redux Toolkit 1.3, we do have a helper method called createAsyncThunk that generates the action creators and does request lifecycle action dispatching for you, but it's still the same standard process.

This sample code from the docs sums up the usage;

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'

// First, create the thunk
const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId, thunkAPI) => {
    const response = await userAPI.fetchById(userId)
    return response.data
  }
)

// Then, handle actions in your reducers:
const usersSlice = createSlice({
  name: 'users',
  initialState: { entities: [], loading: 'idle' },
  reducers: {
    // standard reducer logic, with auto-generated action types per reducer
  },
  extraReducers: (builder) => {
    // Add reducers for additional action types here, and handle loading state as needed
    builder.addCase(fetchUserById.fulfilled, (state, action) => {
      // Add user to the state array
      state.entities.push(action.payload)
    })
  },
})

// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))

See the Redux Toolkit "Usage Guide: Async Logic and Data Fetching" docs page for some additional info on this topic.

Hopefully that points you in the right direction!

Diogenes answered 20/2, 2020 at 16:56 Comment(16)
Thank you. I had seen you commenting on a Git issue but as it was still open, I assumed there was no implementation yet (and thus hadn't ventured much further). Great work by the way and can not wait for the next major release! I am mainly a Spring Boot backend dev and being new the whole front end world makes my head spin but your work is making it so very understandable and use-able. Probably should have looked at the 'Advanced' but having done redux for all of a couple of days (and RTK for a couple of hours) I didn't quite push that far ha!Graphophone
Great, thanks for the very positive feedback! And yeah, the RTK docs are currently written under the assumption you know how Redux works already, so they focus on explaining how using RTK is different than writing Redux logic "by hand". My next task after RTK 1.3.0 is to add a new Redux core docs "Quick Start" page that assumes you're new to Redux, and shows the easiest way to jump into writing Redux code with RTK from scratch.Diogenes
That would be perfect. I have found myself diving in to the world of front end...and in the space of 2 weeks gone through the evolution of: 1. ReactJS with lifecycle and state only. 2. With lifecycle and redux and MaterialUI. 3. With hooks and non-hook redux. 4. With redux-hooks. 5. With RTK. 6.(Today) with the thunk and also trying your rather awesome 1.3.0 alpha with createAsyncThunk(). A 'what do I do to get started with redux for the total noob' would be another great addition imho. Again, thanks for what is a great project!Graphophone
Yipes, that is a lot to throw yourself into! We generally recommend that folks should really only tackle Redux once they're already comfortable using React. That way, there's fewer new concepts to learn at once, and it's more obvious how Redux fits into a React app and why it might be useful. But, yeah, glad RTK is proving helpful! Keep an eye on the Redux docs - I hope to start putting together that "Quick Start" page in the next couple weeks.Diogenes
@markerison How would you monitor an unmount from a functional component using this scenario? I know you can do this using useEffect() and the abort() method on the dispatch call however if the dispatch is on button click dispatch(fetchUserById(123)), then where would you check for unmount?Ripley
In Line 7 you have thunkAPI. It should be userAPI, correct?Auer
No, the example is correct. userAPI is a notionatl abstraction layer around making an AJAX request. thunkAPI is the specific argument that createAsyncThunk passes to your payload creation callbackDiogenes
Using the exact code of the example gives typing error on arguments using TS it seems??? The function created by createAsyncThunk expects 0 arguments,Philipps
Never mind: it seems one must explicitly override the type of any arguments to the 2nd parameter of createAsyncThunk. If left as default, it will assume type voidPhilipps
The example that I showed in this SO answer is written in plain JS. Please see the RTK "Usage with TypeScript docs page section on typing createAsyncThunk for details on how to type it correctly.Diogenes
The redux documentation as well as your answer is missing what the userAPI file looks like @Diogenes - that's what I've been searching around for, and it's really confusing when looking with fresh eyes.Knockabout
@FooBar: There isn't any actual userAPI file or implementation in the docs, because it's a stub that doesn't matter. You could implement an API service layer like that with fetch, axios, XHR, or anything else. The example isn't trying to show "here's how to make the actual server request at the network level". All that matters for this example is "this function takes a user ID and returns a Promise with the data".Diogenes
@Diogenes I am kind of new to React & redux and it's difficult to switch between redux/ redux toolkit documentation. Can you guys update the documentation to provide full details in one placeCotoneaster
@Cotoneaster : Unfortunately it's difficult, because they're separate repositories. Trying to merge all the docs into one site would be a very large effort, and we're doing this maintenance work in our spare time. Sorry :(Diogenes
@Diogenes I have a specific usecase where I'm trying to do something "optimistically" (make the update in redux on client, THEN select new state and persist that to the DB). It looks like we now only have access to state when the thunk is fulfilled. How would this be done with Redux Toolkit (I'm used to the older way of Redux where a Thunk gave you state and dispatch). Would I have to create a custom middleware? Thanks!Naphthalene
@NeilGirardi : could you ask this as a new question, either here on SO or over in the #redux channel in the Reactiflux Discord? It would also help if you could provide code and more details about what you're trying to do.Diogenes
T
20

You can use createAsyncThunk to create thunk action, which can be trigger using dispatch

teamSlice.ts

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
const axios = require("axios");

export const fetchPlayerList = createAsyncThunk(
  "team/playerListLoading",
  (teamId: string) =>
    axios
      .get(`https://api.opendota.com/api/teams/${teamId}/players`)
      .then((response) => response.data)
      .catch((error) => error)
);

const teamInitialState = {
  playerList: {
    status: "idle",
    data: {},
    error: {},
  },
};

const teamSlice = createSlice({
  name: "user",
  initialState: teamInitialState,
  reducers: {},
  extraReducers: {
    [fetchPlayerList.pending.type]: (state, action) => {
      state.playerList = {
        status: "loading",
        data: {},
        error: {},
      };
    },
    [fetchPlayerList.fulfilled.type]: (state, action) => {
      state.playerList = {
        status: "idle",
        data: action.payload,
        error: {},
      };
    },
    [fetchPlayerList.rejected.type]: (state, action) => {
      state.playerList = {
        status: "idle",
        data: {},
        error: action.payload,
      };
    },
  },
});

export default teamSlice;

Team.tsx component

import React from "react";
import { useSelector, useDispatch } from "react-redux";

import { fetchPlayerList } from "./teamSlice";

const Team = (props) => {
  const dispatch = useDispatch();
  const playerList = useSelector((state: any) => state.team.playerList);

  return (
    <div>
      <button
        onClick={() => {
          dispatch(fetchPlayerList("1838315"));
        }}
      >
        Fetch Team players
      </button>

      <p>API status {playerList.status}</p>
      <div>
        {playerList.status !== "loading" &&
          playerList.data.length &&
          playerList.data.map((player) => (
            <div style={{ display: "flex" }}>
              <p>Name: {player.name}</p>
              <p>Games Played: {player.games_played}</p>
            </div>
          ))}
      </div>
    </div>
  );
};

export default Team;
Trapezoid answered 18/7, 2020 at 4:39 Comment(6)
why did you put [fetchPlayerList.fulfilled.type] i.e. .type extra?Decade
@AkshayVijayJain For typescript, I have added it. If you are using es6 only then you can use [fetchPlayerList.fulfilled]Trapezoid
@AkshayVijayJain you saved my life bro. I've only found the builder weird syntax, but that's working too. Just adding these .type at the end. Thank you!Ellison
There is missing explanation on RTK website of how to get action names from createAsyncThunk, thank you for pointing this outMagma
@Ellison I know it's kind of late, but please note that the "weird builder syntax" is how we recommend writing that. The object notation is not something we recommend at this point, due to poort TypeScript and IDE support.Cutback
I think this pattern is more Redux's way of doing things and more declarative by nature. Thanks for sharing.Shuttle
I
6

Use redux-toolkit v1.3.0-alpha.8

Try this

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

export const myAsyncInSlice = createAsyncThunk('bundles/myAsyncInSlice', () =>
  getAxiosInstance()
    .get('/')
    .then(ok => ok.data)
    .catch(err => err),
);

const usersSlice = createSlice({
  name: 'bundles',
  initialState: {
    bundles: [],
    selectedBundle: null,
    page: {
      page: 0,
      totalElements: 0,
      size: 20,
      totalPages: 0,
    },
    myAsyncResponse: null,
    myAsyncResponseError: null,
  },
  reducers: {
    // add your non-async reducers here
  },
  extraReducers: {
    // you can mutate state directly, since it is using immer behind the scenes
    [myAsyncInSlice.fulfilled]: (state, action) => {
      state.myAsyncResponse = action.payload;
    },
    [myAsyncInSlice.rejected]: (state, action) => {
      state.myAsyncResponseError = action.payload;
    },
  },
});


Inflorescence answered 20/2, 2020 at 9:56 Comment(10)
This new way of doing async thunks is so gross. I miss the old days of being able to return an async function.Sombrero
@Jordan, it is at some parts. But also RTK abstracts the boilerplate-y parts of redux, so I can't complain.Inflorescence
@karlmarkxlopez, I like the boilerplate-y parts of redux. They are just idempotent functions. If I wasn't working on a team that had to decide as a whole, I'd abandon RTK. It seems to add nothing except some handholding and it makes the code so much grosser.Sombrero
it seems as though it may at least be worth adding a simpler syntax for synchronous thunks that only need to retrieve state; the input/output should still able to be auto-inferred type-wise so I don't really see a logical reason otherwise.Christabelle
@Christabelle there is a simpler syntax for synchronous reducers, just put it under reducers not extraReducers. And btw, all thunks are asynchronous.Inflorescence
@KarlLopez at that point it is semantics -- the main issue is the cruft of defining them asynchronously vs synchronously. Methods in reducers do not have access to the Thunk API to retrieve state (unless this has changed or I may not have read the docs thoroughly enough?). It is just overly verbose considering the call signature with a function return type makes it inferable -- hence well before RTK middleware was created, thunk middleware worked in TS. The only option really is to wrap a type which is already done often... not the worst but defeats the purpose of a library to some extent.Christabelle
@rob2d, hmm, any methods in the reducers have access to the state, but only the state within that slice. If you need to access to root state, that is not possible at the slice level. You may be doing something against the pattern if you end up doing this.Inflorescence
understood, this is one reason the concept of Thunks API was introduced -- they are used logically the same way when not using RTK query in a reducer. If I am missing something, I am open minded to the possibility of that too, but it would be great to point out exactly what pattern when saying something is anti-pattern. In this case, async thunks were not the first introduction of the API but only an adoption by RTK's extension lib which is more limiting than reducers + thunk middleware are by default which is where many usually complain about the cruft/overhead of the API.Christabelle
@Christabelle I'm happy to help you if you provide a sample repo so that I can have a better understanding of the problem. It does not need to be the actual project, just a reproduction of what you are trying to achieve.Inflorescence
@KarlLopez all good, thanks for the offer. This is more of a theoretical thing where it should be possible to pull the larger state in a reducer using thunk API params when there's no need for asynchronous flow (in the lib design itself, but it is not so purely theoretical) for less verbose reducers. I may try to create a proof of concept on RTK issues so that it's more productive of a discussion there if I can get some time to do so.Christabelle
G
1

Another use case not yet mentioned, is to use reducers(creators) to create async thunks as part of your slice. Though it requires a bit more setup. Example from docs shown below...

import { createSlice, nanoid } from '@reduxjs/toolkit'

interface Item {
  id: string
  text: string
}

interface TodoState {
  loading: boolean
  todos: Item[]
}

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    loading: false,
    todos: [],
  } satisfies TodoState as TodoState,
  reducers: (create) => ({
    deleteTodo: create.reducer<number>((state, action) => {
      state.todos.splice(action.payload, 1)
    }),
    addTodo: create.preparedReducer(
      (text: string) => {
        const id = nanoid()
        return { payload: { id, text } }
      },
      // action type is inferred from prepare callback
      (state, action) => {
        state.todos.push(action.payload)
      }
    ),
    fetchTodo: create.asyncThunk(
      async (id: string, thunkApi) => {
        const res = await fetch(`myApi/todos?id=${id}`)
        return (await res.json()) as Item
      },
      {
        pending: (state) => {
          state.loading = true
        },
        rejected: (state, action) => {
          state.loading = false
        },
        fulfilled: (state, action) => {
          state.loading = false
          state.todos.push(action.payload)
        },
      }
    ),
  }),
})

export const { addTodo, deleteTodo, fetchTodo } = todosSlice.actions
Gore answered 3/5, 2024 at 23:10 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.