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);
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>
);
}
Remove unnecessary users
prop from Users
component.
components/users.js
...imports
export const Users = () => {
return (
<Container
maxWidth="md"
sx={{ margin: '20px auto' }}>
<UsersTable />
</Container>
);
}
Then, update UsersTable
component.
components/users-table.js
...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
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:
- Contains
value
of the input andonChange
function which will updatevalue
through thesetValue
function. - 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);
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>
);
}
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 theTextField
. 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>
);
}
To be continued... (Final part will be soon)
Top comments (0)