DEV Community

Cover image for Build a CRUD App in React with Tanstack Query and Material UI [Final]
Ihor Filippov
Ihor Filippov

Posted on

Build a CRUD App in React with Tanstack Query and Material UI [Final]

If simply reading this tutorial isn't enough for you, you can go here and write code alongside it.

Now, we are faced with a new challenge.

We need to implement the feature of editing existing users. Of course, we could use the AddUserForm component, but let's not complicate things and create a separate form for editing, which we will show under a certain condition.

First, let's start by adding a new field to our table, which will contain an edit button.

components/users-table.js

import { 
  ...other imports
  Button
} from "@mui/material";

export const UsersTable = () => {
  const { users, createEditUserTemplate } = useUsersContext();

  const handleEditUserAction = (user) => {
    createEditUserTemplate(user);
  }

  const renderUsers = (users) => {
    return users.map(user => {
      return (
        <TableRow key={user.id}>
          <TableCell>{user.name}</TableCell>
          <TableCell>{user.email}</TableCell>
          <TableCell>
            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
              <Button
                variant="contained"
                color="info"
                onClick={() => handleEditUserAction(user)}>
                Edit
              </Button>
            </div>
          </TableCell>
        </TableRow>
      );
    })
  }

  return (
    <TableContainer component={Paper}>
      <Table>
        <TableHead>
          <TableRow>
            <TableCell>Name</TableCell>
            <TableCell>Email</TableCell>
            <TableCell>Actions</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {renderUsers(users)}
        </TableBody>
      </Table>
    </TableContainer>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, we have added a new Actions column to our user table. Now, each user has an Edit button, which, when clicked, passes the user's data to the editing template.

Let's now implement this in the UsersProvider.

components/users-provider.js

...code
export const UsersProvider = ({ data, children }) => {
  ...code
  const [editUserTemplate, setEditUserTemplate] = useState(null);

  ...code

  const createEditUserTemplate = (user) => {
    setEditUserTemplate(user);
  }

  const editUser = (userToEdit) => {
    setUsers(users => {
      return users.map(user => {
        if (user.id === userToEdit.id) {
          user.name = userToEdit.name;
          user.email = userToEdit.email;
        }
        return user;
      });
    });
    setEditUserTemplate(null);
  }

  return (
    <UsersContext.Provider value={{ 
      users,
      addUser,
      editUser,
      editUserTemplate,
      createEditUserTemplate
    }}>
      {children}
    </UsersContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, we define two new functions:

  • editUser: finds the user that needs to be updated and returns a new list
  • createEditUserTemplate: simply adds data to the editing template that we are currently using further.

Now, let's implement form for user editing.

components/edit-user-form.js

import { useEffect } from "react";
import { useInput } from "../hooks/use-input";
import { useUsersContext } from "./users-provider";
import { Button, TextField } from "@mui/material";

export const EditUserForm = () => {
  const [nameProps, nameActions] = useInput('');
  const [emailProps, emailActions] = useInput('');

  const { editUser, editUserTemplate } = useUsersContext();

  useEffect(() => {
    const { name, email, company } = editUserTemplate;

    nameActions.update(name);
    emailActions.update(email);

  }, [editUserTemplate]);

  const onSubmit = (e) => {
    e.preventDefault();
    editUser({
      id: editUserTemplate.id,
      name: nameProps.value,
      email: emailProps.value,
    })
  }

  return (
    <form onSubmit={onSubmit} style={{ margin: '24px 0' }}>
      <TextField
        {...nameProps}
        fullWidth
        type="text"
        label="Name"
        variant="outlined"
      />
      <br />
      <br />
      <TextField
        {...emailProps}
        fullWidth
        type="email"
        label="Email"
        variant="outlined"
      />
      <div style={{ textAlign: 'right', marginTop: 12 }}>
        <Button
          variant="contained"
          color="primary"
          type="submit">
          Save
        </Button>
      </div>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

This form has almost the same functionality as the AddUserForm, except that we update the input fields' state based on the data taken from the editUserTemplate.

The final touch is to tell the Users component which form we currently want to show the user.

components/users.js

import { Container } from '@mui/material';
import { UsersTable } from './users-table';
import { AddUserForm } from './add-user-form';
import { EditUserForm } from './edit-user-form';
import { useUsersContext } from './users-provider';

export const Users = () => {
  const { editUserTemplate } = useUsersContext();

  return (
    <Container 
      maxWidth="md" 
      sx={{ margin: '20px auto' }}>
      {editUserTemplate ? <EditUserForm /> : <AddUserForm />}
      <UsersTable />
    </Container>
  );
}
Enter fullscreen mode Exit fullscreen mode

Finally, we are on the home stretch. We've become a bit familiar with how to build a user-friendly layout using Material UI. We've learned how to effectively fetch data from an API using React Query, and we've also set up the overall architecture for our application, small as it may be.

Now it's time to implement user deletion to wrap things up.

First off all, let's add a Delete button for each row in UsersTable.

components/users-table.js

...imports

export const UsersTable = () => {
  const { users, createEditUserTemplate, deleteUser } = useUsersContext();

  ...code

  const handleDeleteUserAction = (user) => {
    deleteUser(user);
  }

  const renderUsers = (users) => {
    return users.map(user => {
      return (
        <TableRow key={user.id}>
          <TableCell>{user.name}</TableCell>
          <TableCell>{user.email}</TableCell>
          <TableCell>
            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
              <Button
                variant="contained"
                color="info"
                onClick={() => handleEditUserAction(user)}>
                Edit
              </Button>
              <Button
                variant="contained"
                color="error"
                onClick={() => handleDeleteUserAction(user)}>
                Delete
              </Button>
            </div>
          </TableCell>
        </TableRow>
      );
    })
  }

  ...code
}
Enter fullscreen mode Exit fullscreen mode

The logic is very similar to what we did earlier. We just take data about user and execute function we received from useUsersContext.

components/users-provider.js

...code

export const UsersProvider = ({ data, children }) => {
  ...code

  const deleteUser = (userToDelete) => {
    setUsers(users => {
      return users.filter(user => user.id !== userToDelete.id)
    });
    setEditUserTemplate(null);
  }

  return (
    <UsersContext.Provider value={{ 
      users,
      addUser,
      editUser,
      editUserTemplate,
      createEditUserTemplate,
      deleteUser
    }}>
      {children}
    </UsersContext.Provider>
  );
}

export const useUsersContext = () => useContext(UsersContext);
Enter fullscreen mode Exit fullscreen mode

That's it! Our application is working as intended, I hope you enjoyed it.

Top comments (0)