⚗️ React Redux CRUD app for beginners [with Hooks]
Tags: react, javascript, redux, css
Published: 2021-01-29 14:06:05

Redux has been and still is the most used library for managing state within React applications. It provides many benefits but can be daunting to learn for beginners learning React. In this tutorial we will build a simple CRUD app using React and Redux.

[Checkout the finished app here](https://pedantic-roentgen-415bc3.netlify.app/)
[Checkout the code here](https://github.com/sanderdebr/redux-crud-tutorial)

# Contents

1. 🔨 Setup
2. 👪 Loading users from state
3. ➕ Adding a new user
4. 🔧 Editing a user
5. 🗑️ Deleting a user
6. ✨ Loading new users asynchronously

# Redux vs React Context API & useReducer
Last week I wrote a tutorial [on how to use React's Context API and useReducer hook](https://dev.to/sanderdebr/flawless-react-state-management-usereducer-and-context-api-1a7g) for managing state. The combinations of these two are awesome and should be, in my opinion, used for small to medium sized applications with not too complex state logic. When your app grows in size, or you want to be properly prepared for that, it's recommended to switch to Redux.

# Benefits of Redux
Why would you add another library for managing state? I thought React is managing the state already? That's true but imagine you have many components and pages and they all need to fetch data from different API's and data sources and manage the state of how the user is interacting with that data and the interface. Quickly your app state can become a mess. The main benefits that I have discovered are:

* **Global state**: Redux keeps all the state in one store, the single source of truth.
* **Predictable**: Using the single store your app has little problems syncing your current state and actions with other parts of your application.
* **Maintainability**: Because Redux has strict guidelines on how to structure the code, your code will be easier to mainting.

Let's get started!

# 🔨 1. Setup

Let's start by creating a new React app with the default configuration:
`$ npx create-react-app redux-crud-app`

First let's remove all the files inside the /src folder except for App.js and index.js. Clear out App.js and let's only return a word for now. Run the app with `$ npm run start`.

**App.js**
```javascript
function App() {
  return (
    <h1>Hi</h1>
  );
}

export default App;
```

Let's add a simple CSS library so our app will look nice. I will use Skeleton CSS for this tutorial. Just go to index.html and add the following line before the ending </head> tag:
`<link rel="stylesheet" 
href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" />` 

The text should now be styled on your localhost. Let's add the standard React router package to handle our different pages as a development dependency:

`$ npm install react-router-dom --save`

**App.js**
```javascript
import { Route, BrowserRouter as Router, Switch } from "react-router-dom";

import React from "react";
import { UserList } from "./features/users/UserList";

export default function App() {
  return (
    <Router>
      <div>
        <Switch>
          <Route path="/">
            <UserList />
          </Route>
          <Route path="/add-user">
            <h1>Add user</h1>
          </Route>
          <Route path="/edit-user">
            <h1>Edit user</h1>
          </Route>
        </Switch>
      </div>
    </Router>
  );
}
```

And add a the UsersList component for the layout:

**/features/users/UserList.jsx**
```javascript
export function UserList() {
  return (
    <div className="container">
      <div className="row">
        <h1>Redux CRUD User app</h1>
      </div>
      <div className="row">
        <div className="two columns">
          <button className="button-primary">Load users</button>
        </div>
        <div className="two columns">
          <button className="button-primary">Add user</button>
        </div>
      </div>
      <div className="row">
        <table class="u-full-width">
          <thead>
            <tr>
              <th>ID</th>
              <th>Name</th>
              <th>Email</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>1</td>
              <td>Dave Gamache</td>
              <td>dave@gmail.com</td>
              <td>
                <button>Delete</button>
                <button>Edit</button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  );
}
```

# 👪 2. Loading users from state

First we have to add the redux store to our application. Let's install react-redux and the redux toolkit:
`$ npm install @reduxjs/toolkit react-redux --save`  

Then create a file store.js with the following code:

**store.js**
```javascript
import { configureStore } from "@reduxjs/toolkit";

export default configureStore({
  reducer: {},
});
```

Later we'll add our Redux functions to mutate the state (reducers) here. Now we need to wrap our application inside the store by using Redux's provider wrapper:

**index.js**
```javascript
import App from "./App";
import { Provider } from "react-redux";
import React from "react";
import ReactDOM from "react-dom";
import store from "./store";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
```

Next up let's add the redux state and add our users to it. Then we'll fetch this state inside our UserList component.

We'll divide up our code into features. In our app we'll only have on feature, the users. Redux calls the collection of the logic per feature *slices*. Let's create one:

**/features/users/usersSlice**
```javascript
import { createSlice } from "@reduxjs/toolkit";

const initialState = [
  { id: "1", name: "Dave Patrick", email: "dave@gmail.com" },
  { id: "2", name: "Hank Gluhwein", email: "hank@gmail.com" },
];

const usersSlice = createSlice({
  name: "users",
  initialState,
  reducers: {},
});

export default usersSlice.reducer;
``` 

Now we'll add our user slice (user part of state) to our store so we can access it anywhere in our application. Redux automatically creates the .reducer function of slices. So we'll add the user slice as follows:

**store.js**
```javascript
import { configureStore } from "@reduxjs/toolkit";
import usersReducer from "./features/users/usersSlice";

export default configureStore({
  reducer: {
    users: usersReducer,
  },
});
```

*I would recommend using Redux DevTools to see the current state and its differences*

Lastly, let's render our user table based on our Redux state. To access the state in Redux we have to use the `useSelector` hook. This is just a function that returns a piece of the state. We can decide which piece we want by providing it with a function. 

We'll ask for the users object in our state. Then we render this array as list of users. 

**UserList.jsx**
```javascript
import { useSelector } from "react-redux";

export function UserList() {
  const users = useSelector((state) => state.users);

  return (
    ...
          <tbody>
            {users.map(({ id, name, email }, i) => (
              <tr key={i}>
                <td>{id}</td>
                <td>{name}</td>
                <td>{email}</td>
                <td>
                  <button>Delete</button>
                  <button>Edit</button>
                </td>
              </tr>
            ))}
          </tbody>
    ...
  );
}
```

And that's how we render the state on the page with Redux, pretty doable right? 😃

# ➕ 3. Adding a new user

First let's create a basic form with hooks to manage the input fields. **Notice that we are not using Redux here to manage the input fields state**. This is because you do not need to put everything in Redux, actually it is better to keep state that is only needed in one component in that component itself. Input fields are the perfect example.

**/features/users/AddUser.jsx**
```javascript
import { useState } from "react";

export function AddUser() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const handleName = (e) => setName(e.target.value);
  const handleEmail = (e) => setEmail(e.target.value);

  return (
    <div className="container">
      <div className="row">
        <h1>Add user</h1>
      </div>
      <div className="row">
        <div className="three columns">
          <label for="nameInput">Name</label>
          <input
            className="u-full-width"
            type="text"
            placeholder="test@mailbox.com"
            id="nameInput"
            onChange={handleName}
            value={name}
          />
          <label for="emailInput">Email</label>
          <input
            className="u-full-width"
            type="email"
            placeholder="test@mailbox.com"
            id="emailInput"
            onChange={handleEmail}
            value={email}
          />
          <button className="button-primary">Add user</button>
        </div>
      </div>
    </div>
  );
}
```

On submit we want to add the user to the state and sent the user back to the UserList component. If something fails, we'll display an error.

First we add a method/function to our Redux user slice. This method is for mutating the state, which Redux calls a reducer. Our method inside reducers receives the user state and the action, in this case the user form field values.

Redux automatically creates an *action* for us that we can use to call this function.

**usersSlice.js**
```javascript
import { createSlice } from "@reduxjs/toolkit";

const initialState = [
  { id: "1", name: "Dave Patrick", email: "dave@gmail.com" },
  { id: "2", name: "Hank Gluhwein", email: "hank@gmail.com" },
];

const usersSlice = createSlice({
  name: "users",
  initialState,
  reducers: {
    userAdded(state, action) {
      state.push(action.payload);
    },
  },
});

export const { userAdded } = usersSlice.actions;

export default usersSlice.reducer;
```

To use this action function, we need to import the useDispatch hook from Redux. We'll check if the fields are not empty and then dispatch the userAdded action with our fields. To generate the correct user ID, we grab the length of our users array in the state and add one to it.

**AddUser.jsx**
```javascript
import { nanoid } from "@reduxjs/toolkit";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router-dom";
import { useState } from "react";
import { userAdded } from "./usersSlice";

export function AddUser() {
  const dispatch = useDispatch();
  const history = useHistory();

  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [error, setError] = useState(null);

  const handleName = (e) => setName(e.target.value);
  const handleEmail = (e) => setEmail(e.target.value);

  const usersAmount = useSelector((state) => state.users.length);

  const handleClick = () => {
    if (name && email) {
      dispatch(
        userAdded({
          id: usersAmount + 1,
          name,
          email,
        })
      );

      setError(null);
      history.push("/");
    } else {
      setError("Fill in all fields");
    }

    setName("");
    setEmail("");
  };

return (
   ...
   {error && error}
          <button onClick={handleClick} className="button-primary">
            Add user
          </button>
   ...
```

We can add users to the store, awesome!

# 🔧 4. Editing a user

To edit a user we'll update our edit button first by linking it to the dynamic /edit-user/{id} page inside our UserList component:

```javascript
<Link to={`/edit-user/${id}`}>
   <button>Edit</button>
</Link>
```

Then we'll add the new reducer to our Redux slice. It will find the user within our state and update it if it exists.

**usersSlice.js**
```javascript
const usersSlice = createSlice({
  name: "users",
  initialState,
  reducers: {
    userAdded(state, action) {
      state.push(action.payload);
    },
    userUpdated(state, action) {
      const { id, name, email } = action.payload;
      const existingUser = state.find((user) => user.id === id);
      if (existingUser) {
        existingUser.name = name;
        existingUser.email = email;
      }
    },
  },
});
```

Our EditUser.jsx file will look very similar to AddUser.jsx except here we take the user id from the URL path using the useLocation hook from react-router-dom:

**EditUser.jsx**
```javascript
import { useDispatch, useSelector } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";

import { useState } from "react";
import { userUpdated } from "./usersSlice";

export function EditUser() {
  const { pathname } = useLocation();
  const userId = pathname.replace("/edit-user/", "");

  const user = useSelector((state) =>
    state.users.find((user) => user.id === userId)
  );

  const dispatch = useDispatch();
  const history = useHistory();

  const [name, setName] = useState(user.name);
  const [email, setEmail] = useState(user.email);
  const [error, setError] = useState(null);

  const handleName = (e) => setName(e.target.value);
  const handleEmail = (e) => setEmail(e.target.value);

  const handleClick = () => {
    if (name && email) {
      dispatch(
        userUpdated({
          id: userId,
          name,
          email,
        })
      );

      setError(null);
      history.push("/");
    } else {
      setError("Fill in all fields");
    }

    setName("");
    setEmail("");
  };

  return (
    <div className="container">
      <div className="row">
        <h1>Edit user</h1>
      </div>
      <div className="row">
        <div className="three columns">
          <label htmlFor="nameInput">Name</label>
          <input
            className="u-full-width"
            type="text"
            placeholder="test@mailbox.com"
            id="nameInput"
            onChange={handleName}
            value={name}
          />
          <label htmlFor="emailInput">Email</label>
          <input
            className="u-full-width"
            type="email"
            placeholder="test@mailbox.com"
            id="emailInput"
            onChange={handleEmail}
            value={email}
          />
          {error && error}
          <button onClick={handleClick} className="button-primary">
            Save user
          </button>
        </div>
      </div>
    </div>
  );
}

```

# 🗑️ 5. Deleting a user

I would like to invite to figure this one out for yourself! It will be a good excerise to practise what we've learned up till this point.

Hereby my solution for the reducer:

```javascript
    userDeleted(state, action) {
      const { id } = action.payload;
      const existingUser = state.find((user) => user.id === id);
      if (existingUser) {
        return state.filter((user) => user.id !== id);
      }
    },
```

You can check the full solution with my code on github.

# ✨ 6. Loading new users asynchronously

*Heads up - the following part is a bit trickier but very valuable to learn!*

A nice feature would be to load in users from an external API. We will use this free one: `https://jsonplaceholder.typicode.com/users`. 

Redux from itself is running code only synchronously. To handle ascync code it was most common to use something called a redux-thunk, which is just a simple function that allows async code as actions.

Nowadays, Redux has a built in feature to add async code. Many tutorials still use redux-thunk but the new `configureStore` function from redux has this built in already.

Let's add the API fetch to our usersSlice:

```javascript
export const fetchUsers = createAsyncThunk("fetchUsers", async () => {
  const response = await fetch("https://jsonplaceholder.typicode.com/users");
  const users = await response.json();
  return users;
});
```

Then inside our slice we will add a property called `extraReducers` that holds a couple functions to handle the return of the API:

* pending
* fulfilled
* rejected

Our API call returns a *Promise* which is an object that represents the status of an asynchronous operation, in our case an API call. Based on the Promise status we'll update our state.

**usersSlicejs**
```javascript
const usersSlice = createSlice({
  name: "users",
  initialState: {
    entities: [],
    loading: false,
  },
  reducers: { ... },
  extraReducers: {
    [fetchUsers.pending]: (state, action) => {
      state.loading = true;
    },
    [fetchUsers.fulfilled]: (state, action) => {
      state.loading = false;
      state.entities = [...state.entities, ...action.payload];
    },
    [fetchUsers.rejected]: (state, action) => {
      state.loading = false;
    },
  },
});
```

We want to fetch this array of users as soon as our app loads and every time a user clicks on the LOAD USERS button.

To load it as soon as our app loads, let's dispatch it before our <App /> component inside index.js:

`store.dispatch(fetchUsers())`

And to dispatch it on our button:

`onClick={() => dispatch(fetchUsers())}`

That's it! We finished building our CRUD app using React, Redux and Hooks.

[You can find the full source code here.](https://github.com/sanderdebr/redux-crud-tutorial)
[And you can checkout the final app here.](https://pedantic-roentgen-415bc3.netlify.app/)

Thanks for following this tutorial, make sure to follow me for more! 😀