Creating a CRUD app in React with Hooks

sanderdebr - Mar 8 '20 - - Dev Community

In this tutorial we will build a create, read, update and delete web application with React using React Hooks. Hooks let us use state and other features in functional components instead of writing class components.

View demo
View code

This tutorial is divided up in the following sections:

  1. Setting up the project
  2. Adding users table
  3. Adding a user
  4. Deleting a user
  5. Updating a user
  6. Using the Effect Hook
  7. Bonus: fetching users from an API

1. Setting up the project

We will start by creating a react app with npm:

npx create-react-app react-crud-hooks

Then browse to this folder and delete everything from the /src folder except App.js, index.js and index.css

For index.css we will use a simple CSS boilerplate called Skeleton which you can find here: http://getskeleton.com/

Add the styles in the /public folder into index.html:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
Enter fullscreen mode Exit fullscreen mode

Then convert App.js to a functional component and add the following set-up. Notice how easy the skeleton CSS boiler plate works:

import React from 'react'

const App = () => {

  return (
    <div className="container">
      <h1>React CRUD App with Hooks</h1>
      <div className="row">
        <div className="five columns">
          <h2>Add user</h2>
        </div>
        <div className="seven columns">
          <h2>View users</h2>
        </div>
      </div>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

2. Adding users table

We will retrieve our user data from a separate file. Let’s create data.js inside /src and add an array called users with a couple user object inside and then export it:

const userList = [
    {
        id: 1,
        name: 'Frank',
        username: 'Frank Degrassi'
    },
    {
        id: 2,
        name: 'Birgit',
        username: 'Birgit Boswald'
    }
];

export default userList;
Enter fullscreen mode Exit fullscreen mode

Then create a folder called /tables and add a file UserTable.jsx. Here we will add a basic table which loops over the users. Notice we are using a ternary operator which is the same as an if/else statement which returns immediately. Also we are destructuring off the object properties so we do not have to rewrite the property again. If there are no users found, we will show an empty cell with some text.

import React from 'react';

const UserTable = (props) => {
    return (
        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Name</th>
                    <th>Username</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                { props.users.length > 0 ? (
                    props.users.map(user => {
                        const {id, name, username} = user;
                        return (
                            <tr>
                                <td>{id}</td>
                                <td>{name}</td>
                                <td>{username}</td>
                                <td>
                                    <button>Delete</button>
                                    <button>Edit</button>
                                </td>
                            </tr>
                        )
                    })
                ) : (
                    <tr>
                        <td colSpan={4}>No users found</td>
                    </tr>
                )   
                }
            </tbody>
        </table>
    )
}

export default UserTable;
Enter fullscreen mode Exit fullscreen mode

The table loops over the users received by App.js through the user props. Let’s add them into App.js and also the functionality to retrieve users from data.js which we will do with useState. Every useState has a getter and a setter.

import React, {useState} from 'react'
import userList from './data.js';
import UserTable from './tables/UserTable';

const App = () => {

  const [users, setUsers] = useState(userList);

  return (
    <div className="container">
      <h1>React CRUD App with Hooks</h1>
      <div className="row">
        <div className="six columns">
          <h2>Add user</h2>
        </div>
        <div className="six columns">
          <h2>View users</h2>
          <UserTable users={users} />
        </div>
      </div>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Make sure to import the UserTable in App.js and add the users as props into UserTable.

3. Adding a user

Next up we will add the functionality to add a user, first by adding the function into App.js which receives the new user from the Add User component which we will create.

The addUser function puts an object containing a new user into our users array of user objects. We do this by using our setUsers from useState function. By using the spread operator we keep the current user array the same. The ID we will just set based on the current amount of users plus one.

const addUser = user => {
    user.id = users.length + 1;
    setUsers([...users, user]);
  }
Enter fullscreen mode Exit fullscreen mode

Then we will pass this function to our Add User component:

<AddUserForm addUser={addUser} />
Enter fullscreen mode Exit fullscreen mode

Which we will create now! Create a folder /forms with a file called AddUserForm.jsx.

import React, {useState} from 'react';

const AddUserForm = (props) => {

    const initUser = {id: null, name: '', username: ''};

    const [user, setUser] = useState(initUser);

    return (
        <form>
            <label>Name</label>
            <input className="u-full-width" type="text" name=name value={user.name} />
            <label>Username</label>
            <input className="u-full-width" type="text" name=username value={user.username} />
            <button className="button-primary" type="submit">Add user</button>
        </form>
    )
}

export default AddUserForm;
Enter fullscreen mode Exit fullscreen mode

Again we are using useState to manage the state of our new user. The initial state of the user values are empty. Now we will add the onChange and onSubmit functions. For handleChange, we destructure off the properties of the event.target object. Then we dynamically set our object keys based on the used input field:

import React, {useState} from 'react';

const AddUserForm = (props) => {

    const initUser = {id: null, name: '', username: ''};

    const [user, setUser] = useState(initUser);

    const handleChange = e => {
        const {name, value} = e.target;
        setUser({...user, [name]: value});
    }

    const handleSubmit = e => {
        e.preventDefault();
        if (user.name && user.username) {
           handleChange(e, props.addUser(user));
        }
    }

    return (
        <form>
            <label>Name</label>
            <input className="u-full-width" type="text" value={user.name} name="name" onChange={handleChange} />
            <label>Username</label>
            <input className="u-full-width" type="text" value={user.username} name="username" onChange={handleChange} />
            <button className="button-primary" type="submit" onClick={handleSubmit} >Add user</button>
        </form>
    )
}

export default AddUserForm;
Enter fullscreen mode Exit fullscreen mode

Great! Now we can add a user. Notice in our handleSubmit we are preventing the default page refresh and also checking if our user.name and user.username actually have both been filled in.

Update: to make sure our new user only gets added when the state has been set for this new user, we pass the addUser function als a callback after handleChange has been finished. This solves the bug if you add the same user quickly after each other.

4. Deleting a user

Now we will add the functionality to delete a user, which is quite simple. We will just filter over our users array and filter out the user which has the ID of the user we want to delete. Again we will use our setUsers function to update the new users state.

UserTable.jsx

<button onClick={() => props.deleteUser(id)}>Delete</button>
Enter fullscreen mode Exit fullscreen mode

App.js

const deleteUser = id => setUsers(users.filter(user => user.id !== id));

<UserTable users={users} deleteUser={deleteUser} />
Enter fullscreen mode Exit fullscreen mode

5. Updating a user

Updating a user is a bit more difficult than adding or deleting a user. First we will set up out form in ./forms/EditUserForm.jsx and import it into App.js. We will just copy our AddUserForm.jsx and change the currentUser to the user we are receiving from App.js:

import React, {useState} from 'react';

const EditUserForm = (props) => {

    const [user, setUser] = useState(props.currentUser);

    const handleChange = e => {
        const {name, value} = e.target;
        setUser({...user, [name]: value});
    }

    const handleSubmit = e => {
        e.preventDefault();
        if (user.name && user.username) props.updateUser(user);
    }

    return (
        <form>
            <label>Name</label>
            <input className="u-full-width" type="text" value={user.name} name="name" onChange={handleChange} />
            <label>Username</label>
            <input className="u-full-width" type="text" value={user.username} name="username" onChange={handleChange} />
            <button className="button-primary" type="submit" onClick={handleSubmit} >Edit user</button>
            <button type="submit" onClick={() => props.setEditing(false)} >Cancel</button>
        </form>
    )
}

export default EditUserForm;
Enter fullscreen mode Exit fullscreen mode

onSubmit we send the updated users back to App.js

In App.js we will use the useState function again to check if the user is currently editing and to decide which user is currently being edited:

const [editing, setEditing] = useState(false);

const initialUser = {id: null, name: '', username: ''};

const [currentUser, setCurrentUser] = useState(initialUser);
Enter fullscreen mode Exit fullscreen mode

We will show the AddUser or EditUser form based on editing state:

<div className="container">
      <h1>React CRUD App with Hooks</h1>
      <div className="row">
        <div className="five columns">
          { editing ? (
            <div>
              <h2>Edit user</h2>
              <EditUserForm 
                currentUser={currentUser}
                setEditing={setEditing}
                updateUser={updateUser}
              />
            </div>
          ) : (
            <div>
              <h2>Add user</h2>
              <AddUserForm addUser={addUser} />
            </div>
          )}
        </div>
        <div className="seven columns">
          <h2>View users</h2>
          <UserTable users={users} deleteUser={deleteUser} editUser={editUser} />
        </div>
      </div>
    </div>
Enter fullscreen mode Exit fullscreen mode

Then we will add our editUser and updateUser functions in App.js:

const editUser = (id, user) => {
  setEditing(true);
  setCurrentUser(user);
}
const updateUser = (newUser) => {
  setUsers(users.map(user => (user.id === currentUser.id ? newUser : user)))
}
Enter fullscreen mode Exit fullscreen mode

Great! Now we can edit our users. Let’s fix the last issue in the next section.

6. Using the Effect Hook

It is currently not possible to change user while editing, we can fix this by using the effect hook. This is similar to componentDidMount() in class components. First make sure to import useEffect in EditUserForm.jsx

useEffect(() => {
    setUser(props.currentUser)
}, [props])
Enter fullscreen mode Exit fullscreen mode

This will make when the component re renders, the props are also updated.

Super! We have finished building our React CRUD app with Hooks.

View demo
View code

7. Bonus: fetching users from an API

Currently we have our data stored in a plain JS file but in most cases you want to fetch your data from an external source/API. In this bonus section we will build a function to fetch the data source asynchronously.

Let's user this free API to fetch three random users:
https://randomuser.me/api/?results=3

Fetching async data is quite simple and we can use multiple solutions for it, for example:

  • Using a library like axios
  • Using promises
  • Using async/await (cleaner style of writing promises).

I like to use the async await method. This is how it looks like:

const fetchData = async (amount) => {
 const response = await fetch(`https://randomuser.me/api/?results=${amount}`);
 const json = await response.json();
 console.log(json);
}
Enter fullscreen mode Exit fullscreen mode

We just put async in front of our function and then we can use await to only execute the next lines of code when that line is finished. We convert the result to JSON and then log the results to the screen. We would place this in our useEffect hook of App.js and fetch the data on component mount but let's go one step further.

We'll create our own custom React Hook by placing the code above in a seperate file and then returning the result and the loading state.

Create a new folder called hooks with a file useAsyncRequest.js with the following code:

import {useState, useEffect} from 'react';

const useAsyncRequest = amount => {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(false);

    useEffect(() => {
        const fetchData = async () => {
            try {
                setLoading(true);
                const response = await fetch(`https://randomuser.me/api/?results=${amount}`);
                const json = await response.json();
                setData(json.results, setLoading(false));
            } catch (err) {
                console.warn("Something went wrong fetching the API...", err);
                setLoading(false);
            }
        }

        if (amount) {
         fetchData(amount);
        }
    }, [amount]);

    return [data, loading]
}

export default useAsyncRequest;
Enter fullscreen mode Exit fullscreen mode

What happens here is the following. With the useEffect hook we are fetching data from the API on the page load. This function will fire every time our amount will change, so only once because our amount will be a fixed number (3 in my example).

I've added a try-catch block to add error handling for the async await request. Then we'll return two state variables: data and loading. These we'll use in our App component.

Import this file inside the App component and add the following:

  const [data, loading] = useAsyncRequest(3);
  const [users, setUsers] = useState(null);

  useEffect(() => {
    if (data) {
      const formattedUsers = data.map((obj, i) => {
        return {
          id: i,
          name: obj.name.first,
          username: obj.name.first + " " + obj.name.last,
        };
      });
      setUsers(formattedUsers);
    }
  }, [data]);
Enter fullscreen mode Exit fullscreen mode

What changed here is that the users or now set as null as default, and as soon as our Hook has given us back the result, we'll set the users to the fetched users.

The data we get back does not suit our userTable component so we have to format the result. I'm doing it here by mapping over the array and for each object returning a new object that we can use in our App.

The useEffect function/hook gets fired everytime our data variable changes. So basically whenever our useAsyncRequest hook is ready with fetching the data. Cool, right!

Finally, we'll update our App component so it only renders the user table when it is not loading and there are actually users:

{loading || !users ? (
          <p>Loading...</p>
        ) : (
          <div className="seven columns">
            <h2>View users</h2>

            <UserTable
              users={users}
              deleteUser={deleteUser}
              editUser={editUser}
            />
          </div>
        )}
Enter fullscreen mode Exit fullscreen mode

Thanks for following this tutorial! Make sure to follow me for more tips and tricks.

View demo
View code

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player