Как создать CRUD-приложение с помощью React Hooks и Context API
Введение
В этой статье будут рассмотрены хуки React (представленные в версии 16.8).
Внедрение Context API решает одну серьезную проблему: сверление пропеллеров. Процесс передачи наших данных от одного компонента к другому через слои вложенных глубоких компонентов. Хуки React позволяют использовать функциональные, а не классовые компоненты. Там, где нам нужно было использовать метод жизненного цикла, нам приходилось использовать подход, основанный на классах. И теперь нам больше не нужно вызывать super(props)
или беспокоиться о методах привязки или ключевом слове this
.
В этой статье вы будете использовать Context API и хуки React вместе для создания полнофункционального приложения CRUD, которое эмулирует список сотрудников. Он будет считывать данные о сотрудниках, создавать новых сотрудников, обновлять данные о сотрудниках и удалять сотрудников. Обратите внимание, что в этом руководстве не будут использоваться внешние вызовы API. Для демонстрации будут использоваться жестко закодированные объекты, которые будут служить состоянием.
Предпосылки
Для выполнения этого урока вам понадобится:
- Локальная среда разработки для Node.js. Следуйте инструкциям по установке Node.js и созданию локальной среды разработки.
- Понимание импорта, экспорта и рендеринга компонентов React. Вы можете ознакомиться с нашей серией статей «Как программировать в React.js».
Это руководство было проверено с помощью Node v15.3.0, npm
v7.4.0, react
v17.0.1, react-router-dom
v5.2.0, tailwindcss-cli
v0.1.2 и tailwindcss
v2.0.2.
Шаг 1 — Настройка проекта
Во-первых, начните с настройки проекта React, используя Create React App со следующей командой:
- npx create-react-app react-crud-employees-example
Перейдите в только что созданный каталог проекта:
- cd react-crud-employees-example
Затем добавьте react-router-dom
в качестве зависимости, выполнив следующую команду:
- npm install react-router-dom@5.2.0
Примечание. Для получения дополнительной информации о React Router обратитесь к нашему руководству по React Router.
Затем перейдите в каталог src
:
cd src
Добавьте сборку Tailwind CSS по умолчанию в свой проект с помощью следующей команды:
- npx tailwindcss-cli@0.1.2 build --output tailwind.css
Примечание. Для получения дополнительной информации о Tailwind CSS обратитесь к нашему руководству по Tailwind CSS.
Затем откройте index.js
в редакторе кода и измените его, чтобы использовать tailwind.css
и BrowserRouter
:
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import './tailwind.css';
import './index.css';
import App from './App';
ReactDOM.render(
<BrowserRouter>
<App />
<BrowserRouter>
document.getElementById('root')
);
На этом этапе у вас будет новый проект React с Tailwind CSS и react-router-dom
.
Шаг 2 — Создание AppReducer и GlobalContext
Сначала в каталоге src
создайте новый каталог context
.
В этом новом каталоге создайте новый файл AppReducer.js
. Этот редюсер будет определять действия CRUD, такие как ADD_EMPLOYEE
, EDIT_EMPLOYEE
и REMOVE_EMPLOYEE
. Откройте этот файл в редакторе кода и добавьте следующие строки кода:
export default function appReducer(state, action) {
switch (action.type) {
case "ADD_EMPLOYEE":
return {
...state,
employees: [...state.employees, action.payload],
};
case "EDIT_EMPLOYEE":
const updatedEmployee = action.payload;
const updatedEmployees = state.employees.map((employee) => {
if (employee.id === updatedEmployee.id) {
return updatedEmployee;
}
return employee;
});
return {
...state,
employees: updatedEmployees,
};
case "REMOVE_EMPLOYEE":
return {
...state,
employees: state.employees.filter(
(employee) => employee.id !== action.payload
),
};
default:
return state;
}
};
ADD_EMPLOEES
примет значение полезной нагрузки, содержащее новых сотрудников, и вернет обновленное состояние сотрудника.
EDIT_EMPLOYEE
примет значение полезной нагрузки и сравнит id
с данными о сотрудниках. Если он найдет совпадение, он будет использовать новые значения полезной нагрузки и вернуть обновленное состояние сотрудника.
REMOVE_EMPLOYEE
примет значение полезной нагрузки и сравнит id
с данными о сотрудниках. Если он найдет совпадение, он удалит этого сотрудника и вернет обновленное состояние сотрудника.
Оставаясь в каталоге context
, создайте новый файл GlobalState.js
. Он будет содержать начальное жестко закодированное значение для имитации данных о сотрудниках, возвращаемых из запроса. Откройте этот файл в редакторе кода и добавьте следующие строки кода:
import React, { createContext, useReducer } from 'react';
import appReducer from './AppReducer';
const initialState = {
employees: [
{
id: 1,
name: "Sammy",
location: "DigitalOcean",
designation: "Shark"
}
]
};
export const GlobalContext = createContext(initialState);
export const GlobalProvider = ({ children }) => {
const [state, dispatch] = useReducer(appReducer, initialState);
function addEmployee(employee) {
dispatch({
type: "ADD_EMPLOYEE",
payload: employee
});
}
function editEmployee(employee) {
dispatch({
type: "EDIT_EMPLOYEE",
payload: employee
});
}
function removeEmployee(id) {
dispatch({
type: "REMOVE_EMPLOYEE",
payload: id
});
}
return (
<GlobalContext.Provider
value={{
employees: state.employees,
addEmployee,
editEmployee,
removeEmployee
}}
>
{children}
</GlobalContext.Provider>
);
};
Этот код добавляет некоторую функциональность для отправки действия, которое переходит в файл редуктора для переключения на случай, соответствующий каждому действию.
На этом этапе у вас должно быть приложение React с AppReducer.js
и GlobalState.js
.
Давайте создадим компонент EmployeeList
, чтобы убедиться, что приложение находится в рабочем состоянии. Перейдите в каталог src
и создайте новый каталог components
. В этом каталоге создайте новый файл EmployeeList.js
и добавьте следующий код:
import React, { useContext } from 'react';
import { GlobalContext } from '../context/GlobalState';
export const EmployeeList = () => {
const { employees } = useContext(GlobalContext);
return (
<React.Fragment>
{employees.length > 0 ? (
<React.Fragment>
{employees.map((employee) => (
<div
className="flex items-center bg-gray-100 mb-10 shadow"
key={employee.id}
>
<div className="flex-auto text-left px-4 py-2 m-2">
<p className="text-gray-900 leading-none">
{employee.name}
</p>
<p className="text-gray-600">
{employee.designation}
</p>
<span className="inline-block text-sm font-semibold mt-1">
{employee.location}
</span>
</div>
</div>
))}
</React.Fragment>
) : (
<p className="text-center bg-gray-100 text-gray-500 py-5">No data.</p>
)}
</React.Fragment>
);
};
Этот код будет отображать employee.name
, employee.designation
и employee.location
для всех employee.code
.
Затем откройте App.js
в редакторе кода. И добавьте EmployeeList
и GlobalProvider
.
import { EmployeeList } from './components/EmployeeList';
import { GlobalProvider } from './context/GlobalState';
function App() {
return (
<GlobalProvider>
<div className="App">
<EmployeeList />
</div>
</GlobalProvider>
);
}
export default App;
Запустите ваше приложение и наблюдайте за ним в веб-браузере:
Компонент EmployeeList
будет отображать жестко запрограммированные значения, установленные в GlobalState.js
.
Шаг 3 — Создание компонентов AddEmployee и EditEmployee
На этом этапе вы создадите компоненты для поддержки создания нового сотрудника и обновления существующего сотрудника.
Теперь вернитесь в каталог components
. Создайте новый файл AddEmployee.js
. Он будет служить компонентом AddEmployee
, который будет включать обработчик onSubmit
для передачи значений поля формы в состояние:
import React, { useState, useContext } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { GlobalContext } from '../context/GlobalState';
export const AddEmployee = () => {
let history = useHistory();
const { addEmployee, employees } = useContext(GlobalContext);
const [name, setName] = useState("");
const [location, setLocation] = useState("");
const [designation, setDesignation] = useState("");
const onSubmit = (e) => {
e.preventDefault();
const newEmployee = {
id: employees.length + 1,
name,
location,
designation,
};
addEmployee(newEmployee);
history.push("/");
};
return (
<React.Fragment>
<div className="w-full max-w-sm container mt-20 mx-auto">
<form onSubmit={onSubmit}>
<div className="w-full mb-5">
<label
className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
htmlFor="name"
>
Name of employee
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:text-gray-600"
value={name}
onChange={(e) => setName(e.target.value)}
type="text"
placeholder="Enter name"
/>
</div>
<div className="w-full mb-5">
<label
className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
htmlFor="location"
>
Location
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
value={location}
onChange={(e) => setLocation(e.target.value)}
type="text"
placeholder="Enter location"
/>
</div>
<div className="w-full mb-5">
<label
className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
htmlFor="designation"
>
Designation
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:text-gray-600"
value={designation}
onChange={(e) => setDesignation(e.target.value)}
type="text"
placeholder="Enter designation"
/>
</div>
<div className="flex items-center justify-between">
<button className="mt-5 bg-green-400 w-full hover:bg-green-500 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
Add Employee
</button>
</div>
<div className="text-center mt-4 text-gray-500">
<Link to="/">Cancel</Link>
</div>
</form>
</div>
</React.Fragment>
);
};
В этом коде setName
, setLocation
и setDesignation
будут принимать текущие значения, которые пользователи вводят в поля формы. Эти значения будут заключены в новую константу newEmployee
с уникальным id
(добавляя единицу к общей длине). Затем маршрут будет изменен на главный экран, на котором будет отображаться обновленный список сотрудников, включая недавно добавленного сотрудника.
Компонент AddEmployee
импортировал GlobalState
и useContext
, один из встроенных хуков React, предоставляя функциональным компонентам легкий доступ к нашему контексту.
Объект employees
, removeEmployee
и editEmployees
были импортированы из файла GlobalState.js
.
Находясь в каталоге components
, создайте новый файл EditEmployee.js
. Он будет служить компонентом editEmployee
, который будет включать функции редактирования существующих объектов из состояния:
import React, { useState, useContext, useEffect } from 'react';
import { useHistory, Link } from 'react-router-dom';
import { GlobalContext } from '../context/GlobalState';
export const EditEmployee = (route) => {
let history = useHistory();
const { employees, editEmployee } = useContext(GlobalContext);
const [selectedUser, setSelectedUser] = useState({
id: null,
name: "",
designation: "",
location: "",
});
const currentUserId = route.match.params.id;
useEffect(() => {
const employeeId = currentUserId;
const selectedUser = employees.find(
(currentEmployeeTraversal) => currentEmployeeTraversal.id === parseInt(employeeId)
);
setSelectedUser(selectedUser);
}, [currentUserId, employees]);
const onSubmit = (e) => {
e.preventDefault();
editEmployee(selectedUser);
history.push("/");
};
const handleOnChange = (userKey, newValue) =>
setSelectedUser({ ...selectedUser, [userKey]: newValue });
if (!selectedUser || !selectedUser.id) {
return <div>Invalid Employee ID.</div>;
}
return (
<React.Fragment>
<div className="w-full max-w-sm container mt-20 mx-auto">
<form onSubmit={onSubmit}>
<div className="w-full mb-5">
<label
className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
htmlFor="name"
>
Name of employee
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
value={selectedUser.name}
onChange={(e) => handleOnChange("name", e.target.value)}
type="text"
placeholder="Enter name"
/>
</div>
<div className="w-full mb-5">
<label
className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
htmlFor="location"
>
Location
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
value={selectedUser.location}
onChange={(e) => handleOnChange("location", e.target.value)}
type="text"
placeholder="Enter location"
/>
</div>
<div className="w-full mb-5">
<label
className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
htmlFor="designation"
>
Designation
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
value={selectedUser.designation}
onChange={(e) => handleOnChange("designation", e.target.value)}
type="text"
placeholder="Enter designation"
/>
</div>
<div className="flex items-center justify-between">
<button className="block mt-5 bg-green-400 w-full hover:bg-green-500 text-white font-bold py-2 px-4 rounded focus:text-gray-600 focus:shadow-outline">
Edit Employee
</button>
</div>
<div className="text-center mt-4 text-gray-500">
<Link to="/">Cancel</Link>
</div>
</form>
</div>
</React.Fragment>
);
};
Этот код использует хук useEffect
, который вызывается при монтировании компонента. Внутри этого хука текущий параметр маршрута будет сравниваться с таким же параметром в объекте employees
из состояния.
Прослушиватели событий onChange
запускаются, когда пользователь вносит изменения в поля формы. userKey
и newValue
передаются в setSelectedUser
. selectedUser
распространяется, и userKey
устанавливается в качестве ключа, а newValue
устанавливается в качестве значения.
Шаг 4 — Настройка маршрутов
На этом шаге вы обновите EmployeeList
, чтобы он ссылался на компоненты AddEmployee
и EditEmployee
.
Пересмотрите EmployeeList.js
и измените его, чтобы использовать Link
и removeEmployee
:
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import { GlobalContext } from '../context/GlobalState';
export const EmployeeList = () => {
const { employees, removeEmployee } = useContext(GlobalContext);
return (
<React.Fragment>
{employees.length > 0 ? (
<React.Fragment>
{employees.map((employee) => (
<div
className="flex items-center bg-gray-100 mb-10 shadow"
key={employee.id}
>
<div className="flex-auto text-left px-4 py-2 m-2">
<p className="text-gray-900 leading-none">
{employee.name}
</p>
<p className="text-gray-600">
{employee.designation}
</p>
<span className="inline-block text-sm font-semibold mt-1">
{employee.location}
</span>
</div>
<div className="flex-auto text-right px-4 py-2 m-2">
<Link
to={`/edit/${employee.id}`}
title="Edit Employee"
>
<div className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold mr-3 py-2 px-4 rounded-full inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
</div>
</Link>
<button
onClick={() => removeEmployee(employee.id)}
className="block bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold py-2 px-4 rounded-full inline-flex items-center"
title="Remove Employee"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-trash-2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
</button>
</div>
</div>
))}
</React.Fragment>
) : (
<p className="text-center bg-gray-100 text-gray-500 py-5">No data.</p>
)}
</React.Fragment>
);
};
Этот код добавит два значка рядом с информацией о сотруднике. Значок карандаша и бумаги обозначает \Edit и указывает на компонент EditEmployee
. Значок корзины представляет \Remove и нажатие на него запускает removeEmployee
.
Далее вы создадите два новых компонента — Heading
и Home
— для отображения компонента EmployeeList
и предоставления пользователям доступа к элементу AddEmployee.
компонент.
В каталоге components
создайте новый файл Heading.js
:
import React from "react";
import { Link } from "react-router-dom";
export const Heading = () => {
return (
<div>
<div className="flex items-center mt-24 mb-10">
<div className="flex-grow text-left px-4 py-2 m-2">
<h5 className="text-gray-900 font-bold text-xl">Employee Listing</h5>
</div>
<div className="flex-grow text-right px-4 py-2 m-2">
<Link to="/add">
<button className="bg-green-400 hover:bg-green-500 text-white font-semibold py-2 px-4 rounded inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
<span className="pl-2">Add Employee</span>
</button>
</Link>
</div>
</div>
</div>
);
};
В каталоге components
создайте новый файл Home.js
:
import React from "react";
import { Heading } from "./Heading";
import { EmployeeList } from "./EmployeeList";
export const Home = () => {
return (
<React.Fragment>
<div className="container mx-auto">
<h3 className="text-center text-3xl mt-20 text-base leading-8 text-black font-bold tracking-wide uppercase">
CRUD with React Context API and Hooks
</h3>
<Heading />
<EmployeeList />
</div>
</React.Fragment>
);
};
Пересмотрите App.js
и импортируйте Route
и Switch
из react-router-dom
. Назначьте компоненты Home
, AddeEmployee
и EditEmployee
каждому маршруту:
import { Route, Switch } from 'react-router-dom';
import { GlobalProvider } from './context/GlobalState';
import { Home } from './components/Home';
import { AddEmployee } from './components/AddEmployee';
import { EditEmployee } from './components/EditEmployee';
function App() {
return (
<GlobalProvider>
<div className="App">
<Switch>
<Route path="/" component={Home} exact />
<Route path="/add" component={AddEmployee} exact />
<Route path="/edit/:id" component={EditEmployee} exact />
</Switch>
</div>
</GlobalProvider>
);
}
export default App;
Скомпилируйте приложение и просмотрите его в своем браузере.
Вы будете перенаправлены на компонент Home
с компонентами Heading
и EmployeeList
:
Нажмите на ссылку «Добавить сотрудника». Вы будете перенаправлены к компоненту AddEmployee
:
После отправки информации о новом сотруднике вы будете перенаправлены обратно в компонент Home
, где теперь будет указан новый сотрудник.
Нажмите на ссылку «Редактировать сотрудника». Вы будете перенаправлены к компоненту EditEmployee
:
После изменения информации о сотруднике вы будете перенаправлены обратно к компоненту Home
, где теперь будет указан новый сотрудник с обновленными данными.
Заключение
В этой статье вы использовали Context API и хуки React вместе для создания полнофункционального приложения CRUD.
Если вы хотите узнать больше о React, загляните на нашу страницу темы React с упражнениями и проектами по программированию.