Thunk with redux-toolkit

Configure async requests with redux-toolkit

react-redux

16-August-2020

As we know as Javascript developer or React Developer specifically, we rely on redux for state management,I know there are a lot of other options but redux still the most popular one between them.

And that makes developers go creative about creating more comfortable, esaiable and maintainable projects, one of them is redux-toolkit. Redux-toolkit makes it easy to create store, configure it with middlewares and connect actions to reducers without a lot of typing and files everywhere.

Fact: The last project I was working on, has 70 reducers and same for api reducers

(Note: if you’re not familiar with redux, I suggest to take look at it otherwise this will be Chinese for you).

In short I would like to describe one part which is Handling Async Requests using Redux Thunk Middleware with redux-toolkit. Async calls are the way to communicate between client and server side, and retrieve data, you could do that with redux but it gets really silly later if you want to modify you actions that’s thunk fix this problem.

Let’s started

1: Installation

I suppose that you know how to create react app, and installing some dependencies that we need it for connect app to redux.

npx create-react-app my-app — template=typescript
yarn install @reduxjs/toolkit react-redux

// Typing if you’re using Typescript

yarn add -D react-redux

2: Configure Store

For creating store it more straightforward we just need to import configureStore for our package

import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import { createLogger } from "redux-logger";

import rootReducer from "./features";

const store = configureStore({
  reducer: rootReducer,
  devTools: true,
  middleware: getDefaultMiddleware().concat([createLogger()]),
});

export default store;

You see I have also installed create-logger, you don’t have but nice to see your logs. rootReducer will be the combined reducers which is simply:

import { combineReducers } from "@reduxjs/toolkit";
import todosReducer from "./todos";

export default combineReducers({
  todos: todosReducer,
});

3: Create Todos State

You see above I have implemented todoReducer, this should be created like this:

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import IStore, { ITodoState } from './types'


const initialState: ITodoState = {
  fetching: false,
  todos: [],
  error: null,
};

const { actions, reducer } = createSlice({
  initialState,
  name: "todos",
  reducers: {
    fetchingStart: (state) => ({ ...state, fetching: true }),
  },
});

export const { fetchingStart } = actions;
export default reducer;
export interface ITodo {
  userId: string;
  id: string;
  title: string;
}

export interface ITodoState {
  fetching: boolean;
  todos: ITodo,
  error?: any
}


export default interface IStore {
  todos: ITodosState;
}

This is simple reducer flow in redux-toolkit. now implementing the thunk.

4: Thunk-middleware implementation

Now comes the interesting part where all this article about. First we need to know what thunk middleware is?

Redux Thunk is a middleware that lets you call action creators that return a function instead of an action object. That function receives the store’s dispatch method, which is then used to dispatch regular synchronous actions inside the body of the function once the asynchronous operations have completed.

Please take a look at the repository of redux thunk code, you’ll surprised what is inside. when it comes to thunk we could simply implement any middleware with redux-toolkit but it provides automatically with thunk but under another name and small changes. createAsyncThunk is our function we need to implement. see below:

import { createAsyncThunk } from '@reduxjs/toolkit'
import IState, { ITodo, AppDispatch } from './types'; //<<--------ADDED
/**
* fetching all todos
*
*/
export const fetchingTodos = createAsyncThunk(
  'todos/fetchingTodos',
   async() => {
    const res = await fetch('https://jsonplaceholder.typicode.com/todos');
    return await res.json();
  }
);

/**
* fetching Todo by Id
* <<-------ADDED
**/
export const fetchTodo = createAsyncThunk<
  ITodo,  // the return type
  string, // first param type of async function
  {       // second param type of async function
    dispatch: AppDispatch;
    state: IState;
    extra: {
      jwt: string;
    };
  }
>("todo/fetchTodo", async (todoId, thunkApi) => {
  const req = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId}`,
    {
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${thunkApi.extra.jwt}`,
      },
    }
  );
  return (await req.json()) as ITodo;
});
//<<-------END

So createAsyncThunk takes three params: First the name of your action creator. Second is a async function that will handle the request and return the result. Third is optional that’ll any data you want to pass. You might ask how can I post data? Well that’s pretty simple also in this example I will use types to make you more aware of all typings.

import { createAsyncThunk } from '@reduxjs/toolkit'
import IState, { ITodo, AppDispatch } from './types'; //<<--------ADDED
/**
* fetching all todos
*
*/
export const fetchingTodos = createAsyncThunk(
  'todos/fetchingTodos',
   async() => {
    const res = await fetch('https://jsonplaceholder.typicode.com/todos');
    return await res.json();
  }
);

/**
* fetching Todo by Id
* <<-------ADDED
**/
export const fetchTodo = createAsyncThunk<
  ITodo,  // the return type
  string, // first param type of async function
  {       // second param type of async function
    dispatch: AppDispatch;
    state: IState;
    extra: {
      jwt: string;
    };
  }
>("todo/fetchTodo", async (todoId, thunkApi) => {
  const req = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId}`,
    {
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${thunkApi.extra.jwt}`,
      },
    }
  );
  return (await req.json()) as ITodo;
});
//<<-------END

That’s will how to post data, doesn’t matter witch api client you use, you might be integrating api client with your redux store witch also preferable.

5: Integrate With Redux

Now we have our action we just need to register in Todos-reducer as an action-creator, there are tow ways to implement our action in reducer first by using an Object defined as extraReducers

import { createSlice } from '@reduxjs/toolkit'
import { fetchingTodos } from './todos-actions'


const { actions, reducer } = createSlice({
  initialState,
  name: "todos",
  reducers: {
    fetchingStart: (state) => ({ ...state, fetching: true }), //<< THIS IS REDUNDANT
  },
  //<<----START ADD ------>>
  extraReducers: {
    //@ts-ignore
    [fetchingTodos.fulfilled]: (state, { payload }) => ({
      ...state,
      todos: payload,
      fetching: false,
    }),
    // @ts-ignore
    [fetchingTodos.rejected]: (state, { payload }) => ({
      ...state,
      error: payload,
      fetching: false,
    }),
  },
  //<<----END ADD ------>>
});

Second is passing an extraReducers as function with builder argument. for example:

...
extraReducers: builder=> {

  builder.addCase(fetchingTodos.fulfilled, (state, {payload}) => {
    // you could do something with payload data before add it to store.
  })
  builder.addCase(fetchingTodos.rejected, (state, {payload}) => {
    // you could do something with payload data before add it to store.
  })
}
...

That’s it.we are done from Redux configuration we just need now to test it I suppose that you know how to connect a component with Redux using react-redux After that you implement your function in componentDidMount or useEffect for hooks and call it just like that:

import React from "react";
import { connect } from "react-redux";

import { fetchingTodos } from "store/features/todos";

interface Props {
  data: ITodoState;
  fetchingTodos: () => void;
}

function App({ data, fetchingTodos }: Props) {
  React.useEffect(() => {
    fetchingTodos();
  }, [ fetchingTodos]);

  return (
    <div className="App">
      <ul>
        {data.todos.map((todo) => (
          <li key={todo.id}>
            <h2>{todo.title}</h2>
            <p>{todo.userId}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

const mapStateToProps = (state: IStore) => ({
  data: state.todos,
});

export default connect(mapStateToProps, {
  fetchingTodos,
})(App);

With that you good to go (be creative).


Thank you for following me (Do it 😈). Don’t forget to check my page for more articles, also I have a YouTube Channel for more tutorials you could check it out and follow it(pls)

facebook Twitter YouTube


Published on CodeReview by Mustafa Alroomi on 16-August-2020