58

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;
Kalle Richter
  • 8,008
  • 26
  • 77
  • 177
Jcov
  • 2,122
  • 2
  • 21
  • 32

3 Answers3

116

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!

phry
  • 35,762
  • 5
  • 67
  • 81
markerikson
  • 63,178
  • 10
  • 141
  • 157
  • 1
    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! – Jcov Feb 20 '20 at 19:25
  • 1
    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. – markerikson Feb 20 '20 at 21:42
  • 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! – Jcov Feb 20 '20 at 21:55
  • 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. – markerikson Feb 20 '20 at 21:57
  • @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? – Azayda Aug 31 '20 at 01:35
  • In Line 7 you have `thunkAPI`. It should be `userAPI`, correct? – Adam Wojnar Jan 19 '21 at 13:24
  • 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 callback](https://redux-toolkit.js.org/api/createAsyncThunk#payloadcreator) – markerikson Jan 19 '21 at 14:15
  • Using the exact code of the example gives typing error on arguments using TS it seems??? The function created by createAsyncThunk expects 0 arguments, – Lars Holdaas Mar 04 '21 at 11:53
  • 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 void – Lars Holdaas Mar 04 '21 at 11:57
  • 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`](https://redux-toolkit.js.org/usage/usage-with-typescript#createasyncthunk) for details on how to type it correctly. – markerikson Mar 04 '21 at 15:55
  • The redux documentation as well as your answer is missing what the userAPI file looks like @markerikson - that's what I've been searching around for, and it's really confusing when looking with fresh eyes. – FooBar Oct 06 '21 at 09:33
  • @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". – markerikson Oct 06 '21 at 14:41
  • @markerikson 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 place – Abhi Apr 07 '22 at 05:35
  • @Abhi : 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 :( – markerikson Apr 07 '22 at 18:15
  • @markerikson 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! – Neil Girardi Oct 01 '22 at 00:03
  • @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. – markerikson Oct 01 '22 at 03:25
19

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;
phwt
  • 1,356
  • 1
  • 22
  • 42
Nikhil Mahirrao
  • 3,547
  • 1
  • 25
  • 20
  • why did you put [fetchPlayerList.fulfilled.type] i.e. .type extra? – Akshay Vijay Jain Jul 22 '20 at 13:32
  • 2
    @AkshayVijayJain For typescript, I have added it. If you are using es6 only then you can use [fetchPlayerList.fulfilled] – Nikhil Mahirrao Aug 06 '20 at 09:45
  • 2
    @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! – sevenlops Dec 01 '20 at 17:28
  • 1
    There is missing explanation on RTK website of how to get action names from createAsyncThunk, thank you for pointing this out – bFunc Jan 12 '21 at 16:03
  • @sevenlops 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. – phry Nov 14 '21 at 09:19
  • 1
    I think this pattern is more Redux's way of doing things and more declarative by nature. Thanks for sharing. – prgmrDev Feb 19 '22 at 14:50
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;
    },
  },
});


karlmarxlopez
  • 1,019
  • 1
  • 8
  • 16
  • This new way of doing async thunks is so gross. I miss the old days of being able to return an async function. – Jordan Nov 04 '22 at 04:15
  • @Jordan, it is at some parts. But also RTK abstracts the boilerplate-y parts of redux, so I can't complain. – karlmarxlopez Nov 04 '22 at 18:18
  • 1
    @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. – Jordan Dec 01 '22 at 18:14