ReactJS: Cách tạo ứng dụng CRUD với React Hook và API Context
Giới thiệu
Bài viết này sẽ trình bày về API Context (được giới thiệu trong phiên bản 16.3).
Sự ra đời của API Context giải quyết một vấn đề lớn: prop-drilling. Quá trình lấy dữ liệu của chúng ta từ component này sang component khác thông qua các lớp của các component sâu lồng nhau. React Hook cho phép sử dụng các component dạng hàm chứ không phải dựa trên lớp. Khi chúng ta cần sử dụng phương pháp vòng đời, chúng ta phải sử dụng cách tiếp cận dựa trên lớp. Và bây giờ chúng ta không còn phải gọi super(props)
hoặc lo lắng về các phương thức ràng buộc hoặc từ khóa this
.
Trong bài viết này, bạn sẽ sử dụng API Context và React Hook để xây dựng một ứng dụng CRUD đầy đủ chức năng mô phỏng danh sách nhân viên. Nó sẽ đọc dữ liệu nhân viên, tạo nhân viên mới, cập nhật dữ liệu nhân viên và xóa nhân viên. Lưu ý rằng hướng dẫn này sẽ không sử dụng bất kỳ lệnh gọi API bên ngoài nào. Để trình diễn, nó sẽ sử dụng các đối tượng được mã hóa cứng sẽ đóng vai trò là state.
Điều kiện tiên quyết
Để hoàn thành bài hướng dẫn này, bạn sẽ cần:
- Môi trường phát triển cục bộ cho Node.js. Làm theo Cách cài đặt Node.js và Tạo Môi trường Phát triển Cục bộ.
- Hiểu biết về nhập, xuất và hiển thị các component React. Bạn có thể xem qua loạt bài Cách viết mã trong React.js của chúng tôi.
Bài hướng dẫn này đã được thực hiện với Node v15.3.0, npm
v7.4.0, react
v17.0.1, react-router-dom
5.2.0, tailwindcss-cli
v 0.1.2 và tailwindcss
v2.0.2.
Bước 1 - Thiết lập dự án
Đầu tiên, hãy bắt đầu với việc thiết lập dự án React bằng cách sử dụng Create React App với lệnh sau:
npx create-react-app react-crud-employees-example
Điều hướng đến thư mục dự án mới được tạo:
cd react-crud-employees-example
Tiếp theo, thêm react-router-dom
dưới dạng một dependency bằng cách chạy lệnh sau:
npm install react-router-dom@5.2.0
Lưu ý: Để biết thêm thông tin về React Router, hãy tham khảo hướng dẫn về React Router của chúng tôi.
Sau đó, điều hướng đến thư mục src
:
cd src
Thêm bản dựng mặc định của Tailwind CSS vào dự án của bạn bằng lệnh sau:
npx tailwindcss-cli@0.1.2 build --output tailwind.css
Lưu ý: Để biết thêm thông tin về Tailwind CSS, hãy tham khảo hướng dẫn Tailwind CSS của chúng tôi.
Tiếp theo, mở index.js
trong trình soạn thảo mã của bạn và sửa đổi nó để sử dụng tailwind.css
và BrowserRouter
:
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') );
Tại thời điểm này, bạn sẽ có một dự án React mới với Tailwind CSS và react-router-dom
.
Bước 2 - Xây dựng AppReducer
và GlobalContext
Đầu tiên, trong thư mục src
, tạo một thư mục mới context
.
Trong thư mục mới này, hãy tạo một file mới có tên AppReducer.js
. Reducer này sẽ định nghĩa các hành động CRUD như ADD_EMPLOYEE
, EDIT_EMPLOYEE
và REMOVE_EMPLOYEE
. Mở tệp này trong trình chỉnh sửa mã của bạn và thêm các dòng mã sau:
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_EMPLOYEES
sẽ nhận một giá trị tải trọng (payload) chứa nhân viên mới và trả về trạng thái nhân viên đã cập nhật.
EDIT_EMPLOYEE
sẽ lấy một giá trị tải trọng và so sánh id
với các nhân viên - nếu tìm thấy sự trùng khớp, nó sẽ sử dụng các giá trị tải trọng mới và trả về trạng thái nhân viên đã cập nhật.
REMOVE_EMPLOYEE
sẽ nhận một giá trị tải trọng và so sánh id
với các nhân viên - nếu tìm thấy sự trùng khớp, nó sẽ loại bỏ nhân viên đó và trả về trạng thái nhân viên đã cập nhật.
Trong thư mục context
, bạn hãy tạo một file mới có tên GlobalState.js
. Nó sẽ chứa một giá trị được mã hóa cứng ban đầu để mô phỏng dữ liệu nhân viên được trả về từ một yêu cầu. Mở tệp này trong trình chỉnh sửa mã của bạn và thêm các dòng mã sau:
import { 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> ); };
Mã này thêm một số chức năng để gửi một hành động đi vào file reducer để chuyển sang trường hợp tương ứng với mỗi hành động.
Tại thời điểm này, bạn sẽ có một ứng dụng React với AppReducer.js
và GlobalState.js
.
Hãy tạo một component EmployeeList
để xác minh rằng ứng dụng đang hoạt động. Điều hướng đến thư mục src
và tạo một thư mục mới có tên components
. Trong thư mục đó, hãy tạo một fle mới có tên EmployeeList.js
và thêm vào mã sau:
import { 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> ); };
Mã này sẽ hiển thị employee.name
, employee.designation
và employee.location
cho tất cả các employees
.
Tiếp theo, mở App.js
ra và thêm EmployeeList
và GlobalProvider
.
import { EmployeeList } from './components/EmployeeList'; import { GlobalProvider } from './context/GlobalState'; function App() { return ( <GlobalProvider> <div className="App"> <EmployeeList /> </div> </GlobalProvider> ); } export default App;
Chạy ứng dụng của bạn và quan sát nó trong trình duyệt web:
Component EmployeeList
sẽ hiển thị các giá trị được mã hóa cứng đã được thiết lập trong GlobalState.js
.
Bước 3 - Xây dựng các component AddEmployee
và EditEmployee
Trong bước này, bạn sẽ xây dựng các component để hỗ trợ tạo nhân viên mới và cập nhật nhân viên hiện có.
Bây giờ, điều hướng trở lại thư mục components
. Tạo một file mới tên AddEmployee.js
, nó sẽ đóng vai trò là component AddEmployee
sẽ bao gồm một trình xử lý onSubmit
để đẩy các giá trị của các trường của form vào state:
import { 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> ); };
Trong đoạn code trên thì setName
, setLocation
và setDesignation
sẽ lấy các giá trị hiện tại mà người dùng nhập vào các trường của form. Các giá trị này sẽ được bao bọc trong một hằng mới là newEmployee
, với một giá trị id
duy nhất (thêm một giá trị vào tổng độ dài). Sau đó, lộ route sẽ được chuyển sang màn hình chính sẽ hiển thị danh sách nhân viên đã cập nhật - bao gồm cả nhân viên mới được thêm vào.
Component AddEmployee
đã import GlobalState
và useContext là một trong những React Hook được tích hợp sẵn, cho phép các thành phần chức năng dễ dàng truy cập vào ngữ cảnh của ta.
Đối tượng employees
, removeEmployee
và editEmployees
đã được import từ file GlobalState.js
.
Khi vẫn ở trong thư mục components
, hãy tạo một file mới là EditEmployee.js
, nó sẽ đóng vai trò là component editEmployee
sẽ bao gồm chức năng chỉnh sửa các đối tượng hiện có từ state:
import { 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> ); };
Đoạn code trên có sử dụng hook useEffect, được gọi khi component được mount. Bên trong hook này, tham số route hiện tại sẽ được so sánh với tham số tương tự trong đối tượng employees
từ state.
Trình nghe sự kiện onChange được kích hoạt khi người dùng thực hiện thay đổi đối với các trường của form. userKey
và newValue
được truyền tới setSelectedUser
. selectedUser
là spread và userKey
được đặt làm khóa và newValue
được đặt làm giá trị.
Bước 4 - Thiết lập các route
Trong bước này, bạn sẽ cập nhật EmployeeList
để liên kết đến các component AddEmployee
và EditEmployee
.
Mở lại EmployeeList.js
và sửa đổi nó để sử dụng Link
và removeEmployee
:
import { 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> ); };
Đoạn mã trên sẽ thêm hai biểu tượng bên cạnh thông tin nhân viên. Biểu tượng bút chì và giấy đại diện cho “Edit” và liên kết đến component EditEmployee
. Biểu tượng thùng rác đại diện cho “Remove” và nhấp vào nó sẽ kích hoạt removeEmployee
.
Tiếp theo, bạn sẽ tạo hai component mới là Heading
và Home
để hiển thị component EmployeeList
và cung cấp cho người dùng quyền truy cập vào component AddEmployee
.
Trong thư mục components
, hãy tạo một file mới là Heading.js
:
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> ); };
Và cũng trong thư mục components
hãy tạo một file mới khác là Home.js
:
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> ); };
Mở lại App.js
và import Route
và Switch
từ react-router-dom
. Chỉ định route cho các component Home
, AddeEmployee
, EditEmployee
.
import { 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;
Biên dịch ứng dụng và quan sát nó trong trình duyệt của bạn.
Bạn sẽ thấy sự điều hướng giữa component Home
với các component Heading
và EmployeeList
:
Nhấp vào liên kết Add Employee bạn sẽ được chuyển đến component AddEmployee
:
Sau khi submit thông tin để tạo một nhân viên mới, bạn sẽ được chuyển trở lại component Home
và bây giờ nó sẽ liệt kê nhân viên mới ra.
Nhấp vào liên kết (Biểu tượng bút và giấy) Edit Employee và bạn sẽ được chuyển đến component EditEmployee
:
Sau khi sửa đổi thông tin cho nhân viên, bạn sẽ được chuyển trở lại component Home
và bây giờ nó sẽ liệt kê nhân viên mới với các chi tiết được cập nhật.
Phần kết luận
Trong bài viết này, bạn đã sử dụng hook API Context và React Hook để xây dựng một ứng dụng CRUD với đầy đủ các chức năng.
Nếu bạn muốn tìm hiểu thêm về React, hãy xem loạt bài Cách viết mã trong React.js của chúng tôi.