DEV Community

Cover image for Redux-Toolkit CRUD example with React Hooks
Julfikar Haidar
Julfikar Haidar

Posted on

Redux-Toolkit CRUD example with React Hooks

Redux Redux is a open-source Javascript library for managing application stae.In this article,I will show you how to build a React Redux Hooks consume Rest API with axios.

Note: I assume that you are familiar with redux concepts. If you are new to redux,I strongly solicitation you to learn basic concept of redux.

Before we jump into the article, let me show you what we are going to create in this article.

Image

Why to choose Redux Toolkit

  • Easy way to setup store
  • Support some build in dependency as like Immer js, Redux,Redux thank,Reselect,Redux devtools extension.
  • No more write boilerplate

How to setup Create-React-App With Redux

For this redux tutorial lets start with setup new react application:

npx create-react-app my-app
cd my-app
npm start
Enter fullscreen mode Exit fullscreen mode

Next we will add redux with:

npm install @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode

Add React Router

npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

Let’s install axios with command:

npm install axios

Enter fullscreen mode Exit fullscreen mode
import axios from "axios";
const API = axios.create({baseURL: process.env.REACT_APP_BASEURL});

API.interceptors.request.use((req) => {
    if (localStorage.getItem("user")) {
      req.headers.Authorization = `Bearer ${
        JSON.parse(localStorage.getItem("user")).token
      }`;
    }
    return req;
  });

export default API
Enter fullscreen mode Exit fullscreen mode
  • You can change the baseURL that depends on REST APIs url that your Server configures.

Firstly configure store. Create file src/redux/store.js containing:

import { configureStore } from "@reduxjs/toolkit";
import TourReducer from "./features/tourSlice";

export default configureStore({
  reducer: {
    tour: TourReducer,
  },
});

Enter fullscreen mode Exit fullscreen mode

Then we need to connect our store to the React application. Import it into index.js like this:

import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import store  from './redux/store';
import reportWebVitals from './reportWebVitals';
import './index.css';
import App from "./App";

const container = document.getElementById('root');
const root = createRoot(container);

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

Enter fullscreen mode Exit fullscreen mode

Create Slice Reducer and Actions

Instead of creating many folders and files for Redux (actions, reducers, types,…), with redux-toolkit we just need add one file: slice.

A slice is a collection of Redux reducer logic and actions for a single feature.Reducer are pure function which handle all logic on action type.
For creating a slice, we need:

  • name to identify slice
  • initial state
  • one or more reducer functions to define how the state can
    be updated

  • Once a slice is created, we can export the generated Redux action creators and the reducer function for the whole slice.

  • Redux Toolkit provides createSlice() function that will
    auto-generate the action types and action creators for you,
    based on the names of the reducer functions you provide.

Example:

import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    // add your non-async reducers here
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    }
  },
  extraReducers: {
    // extraReducers handles asynchronous requests, which is our main focus.
  }
})
// Action creators
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
Enter fullscreen mode Exit fullscreen mode
  • Redux requires that we write all state updates immutably, by making copies of data and updating the copies. However, Redux Toolkit's createSlice and createReducer APIs use Immer inside to allow us to write "mutating" update logic that becomes correct immutable updates.

Let's create a Slice for src/redux/feature/slice

  • We need to use Redux Toolkit createAsyncThunk which
    provides a thunk that will take care of the action types
    and dispatching the right actions based on the returned
    promise.

  • Asynchronous requests created with createAsyncThunk accept
    three parameters: an action type string, a callback
    function (referred to as a payloadCreator), and an options
    object.

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import API from "../api";

export const createTour = createAsyncThunk(
  "tour/createTour",
  async ({ updatedTourData, navigate, toast }, { rejectWithValue }) => {
    try {
      const response = await API.post("/tour", updatedTourData);
      toast.success("Added Successfully");
      navigate("/dashboard");
      return response.data;
    } catch (err) {
      return rejectWithValue(err.response.data);
    }
  }
);


export const getToursByUser = createAsyncThunk(
  "tour/getToursByUser",
  async (userId, { rejectWithValue }) => {
    try {
      const response = await API.get(`/tour/userTours/${userId}`);;
      return response.data;
    } catch (err) {
      return rejectWithValue(err.response.data);
    }
  }
);


export const updateTour = createAsyncThunk(
  "tour/updateTour",
  async ({ id, updatedTourData, toast, navigate }, { rejectWithValue }) => {
    try {
      const response = await API.patch(`/tour/${id}`, updatedTourData);
      toast.success("Tour Updated Successfully");
      navigate("/dashboard");
      return response.data;
    } catch (err) {
      return rejectWithValue(err.response.data);
    }
  }
);

export const deleteTour = createAsyncThunk(
  "tour/deleteTour",
  async ({ id, toast }, { rejectWithValue }) => {
    try {
      const response = await API.delete(`/tour/${id}`);
      toast.success("Tour Deleted Successfully");
      return response.data;
    } catch (err) {
      return rejectWithValue(err.response.data);
    }
  }
);


const tourSlice = createSlice({
  name: "tour",
  initialState: {
    tour: {},
    tours: [],
    userTours: [],
    tagTours: [],
    relatedTours: [],
    currentPage: 1,
    numberOfPages: null,
    error: "",
    loading: false,
  },
  reducers: {
    setCurrentPage: (state, action) => {
      state.currentPage = action.payload;
    },
  },
  extraReducers: {
    [createTour.pending]: (state, action) => {
      state.loading = true;
    },
    [createTour.fulfilled]: (state, action) => {
      state.loading = false;
      state.tours = [action.payload];
    },
    [createTour.rejected]: (state, action) => {
      state.loading = false;
      state.error = action.payload.message;
    },
    [getToursByUser.pending]: (state, action) => {
      state.loading = true;
    },
    [getToursByUser.fulfilled]: (state, action) => {
      state.loading = false;
      state.userTours = action.payload;
    },
    [getToursByUser.rejected]: (state, action) => {
      state.loading = false;
      state.error = action.payload.message;
    },

    [updateTour.pending]: (state, action) => {
      state.loading = true;
    },
    [updateTour.fulfilled]: (state, action) => {
      state.loading = false;
      const {
        arg: { id },
      } = action.meta;
      if (id) {
        state.userTours = state.userTours.map((item) =>
          item._id === id ? action.payload : item
        );
        state.tours = state.tours.map((item) =>
          item._id === id ? action.payload : item
        );
      }
    },
    [updateTour.rejected]: (state, action) => {
      state.loading = false;
      state.error = action.payload.message;
    }
    ,
    [deleteTour.pending]: (state, action) => {
      state.loading = true;
    },
    [deleteTour.fulfilled]: (state, action) => {
      state.loading = false;
      const {
        arg: { id },
      } = action.meta;
      if (id) {
        state.userTours = state.userTours.filter((item) => item._id !== id);
        state.tours = state.tours.filter((item) => item._id !== id);
      }
    },
    [deleteTour.rejected]: (state, action) => {
      state.loading = false;
      state.error = action.payload.message;
    },

  },
});

export const { setCurrentPage } = tourSlice.actions;

export default tourSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

tour/createTour is the action type string in this case. Whenever this function is dispatched from a component within our application, createAsyncThunk generates promise lifecycle action types using this string as a prefix:

pending: tour/createTour/pending
fulfilled: tour/createTour/fulfilled
rejected: tour/createTour/rejected

  1. On its initial call, createAsyncThunk dispatches the tour/createTour/pending lifecycle action type. The payloadCreator then executes to return either a result or an error.

  2. In the event of an error, tour/createTour/rejected is dispatched and createAsyncThunk should either return a rejected promise containing an Error instance, a plain descriptive message, or a resolved promise with a RejectWithValue argument as returned by the thunkAPI.rejectWithValue function (more on thunkAPI and error handling momentarily).

  3. If our data fetch is successful, the posts/getPosts/fulfilled action type gets dispatched.

This is how I handle rest of the requests similar way.

Now we move on to the component section

By using useSelector and useDispatch from react-redux, we can read state from a Redux store and dispatch any action from a component, respectively.

Let’s set up a component to dispatch createTour when it mounts:
File AddEditTour.js:

import React, { useState, useEffect } from "react";
import { toast } from "react-toastify";
import { useNavigate, useParams } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { createTour, updateTour } from "../redux/features/tourSlice";
// import TagInput from '../components/TagInput'
import '../components/Tags.css';

const initialState = {
  title: "",
  description: "",
  tags: [],
};

export default function AddEditTour() {
  const [tourData, setTourData] = useState(initialState);
  const [tagErrMsg, setTagErrMsg] = useState(null);
  const { error, userTours } = useSelector((state) => ({
    ...state.tour,
  }));
  const { user } = useSelector((state) => ({ ...state.auth }));
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const { id } = useParams();

  const { title, description, tags } = tourData;


  useEffect(() => {
    if (id) {
      const singleTour = userTours.find((tour) => tour._id === id);
      console.log(singleTour);
      setTourData({ ...singleTour });
    }

  }, [id]);

  useEffect(() => {
    error && toast.error(error);
  }, [error]);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!tags.length) {
      setTagErrMsg("Please provide some tags");
    }
    if (title && description && tags) {
      const updatedTourData = { ...tourData, name: user?.result?.name };

      if (!id) {
        dispatch(createTour({ updatedTourData, navigate, toast }));
      } else {
        dispatch(updateTour({ id, updatedTourData, toast, navigate }));
      }
      handleClear();
    }
  };

  const onInputChange = (e) => {
    const { name, value } = e.target;
    setTourData({ ...tourData, [name]: value });
  };

  const handleClear = () => {
    setTourData({ title: "", description: "", tags: [] });
  };

  const removeTagData = deleteTag => {

    setTourData({
      ...tourData,
      tags: tourData.tags.filter((tag) => tag !== deleteTag),
    });
  };

  const addTagData = event => {

    setTagErrMsg(null);
    if (event.target.value !== '') {
      setTourData({ ...tourData, tags: [...tourData.tags, event.target.value] });
      event.target.value = '';
    }
  };


  const onImageChange = event => {
    console.log(event.target.files[0]);
    let files = event.target.files;
    let reader = new FileReader();
    reader.readAsDataURL(files[0]);

    reader.onload = (e) => {

      setTourData({ ...tourData, imageFile: e.target.result })
    }


  };

  return (


    <>
     <div className="container-fluid">
        <div className="form-box">
          <h1>Add</h1>

          <form onSubmit={handleSubmit}>
            <div className="form-group">
              <label htmlFor="name">Name</label>
              <input className="form-control" id="name" type="text" value={title || ""} name="title" placeholder="Name" onChange={onInputChange} />
            </div>
            <div className="form-group">
              <label htmlFor="email">Image</label>
              <input className="form-control" accept="image/*" onChange={onImageChange}  type="file" />
            </div>
            <div className="form-group">
              <label htmlFor="message">Tag</label>
              <div className="tag-input">
                <ul className="tags">
                  {tags && tags.map((tag, index) => (
                    <li key={index} className="tag">
                      <span className="tag-title">{tag}</span>
                      <span
                        className="tag-close-icon"
                        onClick={() => removeTagData(tag)}
                      >
                        x
                      </span>
                    </li>
                  ))}
                </ul>
                <input
                  className="tag_input"
                  type="text"
                  onKeyUp={event => (event.key === 'Enter' ? addTagData(event) : null)}
                  placeholder="Press enter to add a tag"
                />
              </div>
            </div>
            <div className="form-group">
              <label htmlFor="message">Message</label>
              <textarea className="form-control" id="message" value={description} name="description" placeholder="description" onChange={onInputChange} />
            </div>

            <input className="btn btn-primary" type="submit" defaultValue="Submit" />
          </form></div>


      </div>
    </>
  )
}

Enter fullscreen mode Exit fullscreen mode

First, we define and set initial state.

Next, we create handleInputChange() function to track the values of the input and set that state for changes.

We have local state and send the POST request to the Web API. It dispatchs async Thunk createTour() with useDispatch(). This hook returns a reference to the dispatch function from the Redux store.We check dashboard component then see the difference new data added.When we updated existence data click edit button we go through the same component AddEdittour.js file now we get id and conditionally render data and finally updated data.we have deleted in the same way.

file Dashboard.js

import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { deleteTour, getToursByUser } from "../redux/features/tourSlice";
import Spinner from "../components/Spinner";
import { toast } from "react-toastify";

export default function DashBoard() {

  const { user } = useSelector((state) => ({ ...state.auth }));
  const { userTours, loading } = useSelector((state) => ({ ...state.tour }));
  const userId = user?.result?._id;
  const dispatch = useDispatch();

  useEffect(() => {
    if (userId) {
      dispatch(getToursByUser(userId));
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [userId]);

  const excerpt = (str) => {
    if (str.length > 40) {
      str = str.substring(0, 40) + " ...";
    }
    return str;
  };

  if (loading) {
    return <Spinner />;
  }

  const handleDelete = (id) => {
    if (window.confirm("Are you sure you want to delete this tour ?")) {
      dispatch(deleteTour({ id, toast }));
    }
  };

  return (
    <>

      <div className="container mt-5">
        <div className="row">
          <div className="col-md-12 text-center ">
          <Link to={`/add`} href="#" className="card-link">Add Data</Link>
            {userTours.length === 0 && (
              <h3 className="text-center">No tour available with the user: {user?.result?.name}</h3>

            )}

            {userTours.length > 0 && (
              <>
                <h5 className="text-center">Dashboard: {user?.result?.name}</h5>
                <hr style={{ maxWidth: "570px" }} />
              </>
            )}
          </div>

          {userTours &&
            userTours.map((item,index) => (
              <div className='col-md-3' key={index}>
                <div className="card mb-3" >
                  <img src={item.imageFile} className="card-img-top img-thumbnail rounded" alt={item.title} />
                  <div className="card-body">
                    <h5 className="card-title">{item.title}</h5>
                    <p className="card-text"> {excerpt(item.description)}</p>
                    <Link to={`/edit/${item._id}`} href="#" className="card-link">Edit</Link>
                    <Link  to="#" className="card-link" onClick={() => handleDelete(item._id)}>Delete</Link>
                    <Link to={`/view/${item._id}`} href="#" className="card-link">View</Link>

                  </div>
                </div>

              </div>
            ))}

        </div>
      </div>

    </>
  )
}

Enter fullscreen mode Exit fullscreen mode

I hope you guys liked this simple Redux-toolkit crud operation.You can find complete code repository presented in this article at GitHub.

Top comments (10)

Collapse
 
muhammadwasi81 profile image
Muhammad wasi

Hey! I have clone the repo but it is now working can you please provide me the backend?

Collapse
 
julfikarhaidar profile image
Julfikar Haidar

Yeah sure give me your mail address I will share google drive...

Collapse
 
zakirkhan777 profile image
Zakir-khan-777

julfikar Haider bro, AOA,

can you please share the project with me please..and i ll be needing your bit help as well thanks
prog.salman777@gmail.com

Collapse
 
inam337 profile image
Inam337

I have clone this repo but its not working without backend can you please shared me the backend ?

Collapse
 
julfikarhaidar profile image
Julfikar Haidar

Give me your mail address I will share google drive...

Collapse
 
hrithik812 profile image
Hrithik Rudra

Nice blog,bro.Keep shining like this

Collapse
 
julfikarhaidar profile image
Julfikar Haidar

Thanks..

Collapse
 
sarojghising profile image
Saroj Ghising • Edited

Hey!! can you please provide me the backend code ?

my email : sarojghising13@gmail.com (to share backend code.)

Collapse
 
mottyx44 profile image
mottyx44 • Edited

Hello. Sir can you share the backend code for this ? Im learning RTK asyncThunk atm. And I think this one will help for simulation.

Collapse
 
mwaqar948 profile image
Muhammad Waqar

Julfikar Haidar can you please share backend code for this React CRUD?