Build a CRUD App in React with Tanstack Query and Material UI [Part 2]
Tags: react, reactquery, tutorial, beginners
Published: 2024-01-15 15:13:32

*If simply reading this tutorial isn't enough for you, you [can go here and write code alongside](https://www.frontendly.dev/ihor-filippov/build-a-crud-app-in-react-with-tanstack-query-and-material-ui/3) 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](https://www.frontendly.dev/ihor-filippov/react-patterns-in-action/1).

`components/users-provider.js`
```jsx
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);
```
And, then wrap `Users` with it.

`App.js`
```jsx
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>
  );
}
```
Remove unnecessary `users` prop from `Users` component.

`components/users.js`
```jsx
...imports
export const Users = () => {
  return (
    <Container 
      maxWidth="md" 
      sx={{ margin: '20px auto' }}>
      <UsersTable />
    </Container>
  );
}
```
Then, update `UsersTable` component.

`components/users-table.js`
```jsx
...imports
import { useUsersContext } from './users-provider';

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

  ...code
}
```

## Add new user

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

`hooks/useInput.js`
```jsx
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)
    }
  ];
}
```
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`
```jsx
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);
```
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`
```jsx
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>
  );
}
```
`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`
```jsx
...imports
import { AddUserForm } from './add-user-form';

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

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