ReactJS: Cách tạo ứng dụng CRUD với React Hook và API Context


Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên

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:

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-cliv 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_EMPLOYEEEDIT_EMPLOYEEvà 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.nameemployee.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:

Ảnh chụp màn hình các giá trị nhân viên được mã hóa cứng với tên, chỉ định và vị trí

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ì setNamesetLocation 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 employeesremoveEmployee 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 setSelectedUserselectedUser 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 HomeAddeEmployeeEditEmployee.

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 chụp màn hình của thành phần Home

Nhấp vào liên kết Add Employee bạn sẽ được chuyển đến component AddEmployee:

Ảnh chụp màn hình của thành phần AddEaffee

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:

Ảnh chụp màn hình của thành phần EditE Employee

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.

» Tiếp: Cách sử dụng Axios với React
« Trước: Cách bật tính năng kết xuất phía máy chủ cho ứng dụng React
Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên
Copied !!!