Redux-Toolkit CRUD example with React Hooks
Tags: react, redux, reduxtoolki, javascript
Published: 2022-06-06 19:32:17

**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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xw2fzivdg63llaoxs8n7.gif)

## 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
```

Next we will add redux with:

```
npm install @reduxjs/toolkit react-redux
```


Add React Router 

```
npm install react-router-dom
```


Let’s install axios with command: 

```
npm install axios

```


```
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
```


- 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,
  },
});

```


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>
);

```

## 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;
```

- 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;
```

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>
    </>
  )
}

```
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>

    </>
  )
}

```

I hope you guys liked this simple Redux-toolkit crud operation.You can find complete code repository presented in this article at [GitHub](https://github.com/Julfikar-Haidar/redux-toolkit).