DEV Community

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

Posted on • Updated on

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

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

Architecture enhancement

Although our project is quite small, we have already encountered a situation where we are dealing with prop-drilling.

Let's refresh our knowledge about it.

Prop drilling involves passing data or state across multiple levels of a component structure. Essentially, it refers to the practice of transferring data from a parent component to its child components, and then from these children to their subsequent children, continuing this process until the data reaches the desired component.

Here are some key issues commonly associated with prop-drilling:

  • When a top-level component's state changes, all children components in the prop-drilling chain will re-render, even if they don't directly use the changed data. This can lead to performance issues, especially in large applications with deep component trees.
  • Due to the tight coupling and complex data flow, refactoring becomes a challenge. Changing the structure or logic of one component may necessitate changes in all components involved in the prop-drilling chain.
  • As your application grows, prop-drilling leads to more complex component trees, making it harder to track and manage data flow. This complexity can make the codebase less maintainable, as changes in one component might require adjustments in many others.

That's why we need to get rid of it as soon as possible.

Let's do it, with the help of Compound Pattern.

components/users-provider.js

import { createContext, useContext, useState } from "react";

const UsersContext = createContext();

export const UsersProvider = ({ data, children }) => {
  const [users, setUsers] = useState(data);

  return (
    <UsersContext.Provider value={{ users }}>
      {children}
    </UsersContext.Provider>
  );
}

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

And, then wrap Users with it.

App.js

import { UsersProvider } from './components/users-provider';
import { Users } from './components/users';
import { useUsers } from './api/use-users';

export default function App() {
  const { isFetching, isError, data } = useUsers();

  if (isFetching) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>Error</div>;
  }

  return (
    <UsersProvider data={data}>
      <Users />
    </UsersProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Remove unnecessary users prop from Users component.

components/users.js

...imports
export const Users = () => {
  return (
    <Container 
      maxWidth="md" 
      sx={{ margin: '20px auto' }}>
      <UsersTable />
    </Container>
  );
}
Enter fullscreen mode Exit fullscreen mode

Then, update UsersTable component.

components/users-table.js

...imports
import { useUsersContext } from './users-provider';

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

  ...code
}
Enter fullscreen mode Exit fullscreen mode

Add new user

First, let's define a useInput custom hook to make it easier to manage input control properties.

hooks/useInput.js

import { useState } from 'react';

export const useInput = (initialValue) => {
  const [value, setValue] = useState(initialValue);

  return [
    {
      value,
      onChange: (e) => setValue(e.target.value)
    },
    {
      update: (v) => setValue(v),
      reset: () => setValue(initialValue)
    }
  ];
}
Enter fullscreen mode Exit fullscreen mode

In the code above useInput returns an array with two objects:

  1. Contains value of the input and onChange function which will update value through the setValue function.
  2. Contains methods to update and reset current state of the input.

When useInput hook is done, we are ready to move further.

components/users-provider.js

import { createContext, useContext, useState } from "react";
import { v4 as uuid } from 'uuid';

const UsersContext = createContext();

export const UsersProvider = ({ data, children }) => {
  const [users, setUsers] = useState(data);

  const addUser = (name, email) => {
    const newUser = {
      id: uuid(),
      name,
      email,
    }

    setUsers(users => [...users, newUser]);
  }

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

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

Here we just add a new addUser function and pass it inside the value prop of the UsersContext.Provider.

Since every user fetched from the API has its id, we add our own ids to new users. That's why uuid package was applied here.

components/add-user-form.js

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

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

  const { addUser } = useUsersContext();

  const onSubmit = (e) => {
    e.preventDefault();
    addUser(nameProps.value, emailProps.value);
    nameActions.reset();
    emailActions.reset();
  }

  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">
          Create
        </Button>
      </div>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

TextField is a component from Material UI library used for text input.

  • fullWidth: A boolean prop that, when set to true, makes the TextField stretch to its container's full width.
  • label: sets the label of input field
  • variant: determines the style of the TextField. In this case, outlined means the text field will have an outlined border.

Then, let's place our form into Users component and enjoy the result.

components/users.js

...imports
import { AddUserForm } from './add-user-form';

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

To be continued... (Final part will be soon)

Top comments (0)