Tạo 3 select input để chọn địa chỉ giao hàng cùng React (P2)

Tiếp nối phần 1, ở phần này, mình sẽ đi sâu vào phân tích logic trong việc tải thông tin các đơn vị hành chính vào các select input và xử lí các event cụ thể.

Source code bài viết có thể tham khảo tại Github và demo tại Codesandbox.

Demo của mình cho bài viết

Một số lưu ý

Các bạn có thể tham khảo lại phần 1 để có cái nhìn chi tiết hơn về phần phân tích, setup, giới thiệu về thư viện React Select và các state cần quản lí nhé.

Dưới đây là một số lưu ý cũng như link về technical sẽ sử dụng trong bài viết:

Thông thường, khi cần viết custom hook liên quan đến state management của một component, một form, hay một page, thông thường mình sẽ tiếp cận theo các bước như sau:

  • State define: Xác định các data cần quản lí.

  • Event define: Xác định các event dẫn đến việc thay đổi dữ liệu.

  • Hook define: Định nghĩa state và các event cho custom hook.

  • Fetch define: Định nghĩa/xác định các hàm fetch data.

  • Implement: Thực hiện code cho các event đã định nghĩa.

  • Clean code: Cleanup và chia nhỏ các đoạn code nếu có thể.

Về state define và event define mình đã phân tích ở phần 1, nên trong phần này mình sẽ phân tích các bước còn lại. Và vì source code đã có trong demo, nên trong bài viết này thì mình sẽ chủ yếu phân tích các hàm theo mã giả (Pseudocode).


Định nghĩa custom hook

Từ các state và event, ta có thể dễ dàng định nghĩa custom hook như sau:

function useLocationForm(shouldFetchInitialLocation) {
    const [state, setState] = useState({
        cityOptions: [],
        districtOptions: [],
        wardOptions: [],
        selectedCity: null,
        selectedDistrict: null,
        selectedWard: null,
    });

    useEffect(() => {
        // First-load logic
    }, []);

    function onCitySelect(option) {
        // Logic khi chọn Tỉnh/Thành
    }

    function onDistrictSelect(option) {
        // Logic khi chọn Phường/Xã
    }

    function onWardSelect(option) {
        // Logic khi chọn Quận/Huyện
    }

    return {state, onCitySelect, onDistrictSelect, onWardSelect};
}

export default useLocationForm;

Ở đây mình sử dụng biến shouldFetchInitialLocation để đánh dấu rằng dữ liệu được tạo mới hay sẽ được load từ địa chỉ có sẵn, dĩ nhiên tùy theo business, bạn có thể tùy biến cách khai báo hoặc kiểm soát khác nhau để phục vụ first-load logic.

Tiếp theo là sẽ gắn các state và event vào file JSX/TSX từ custom hook.

import useLocationForm from "./useLocationForm";
import Select from "react-select";

function LocationForm() {
  const {
    state,
    onCitySelect,
    onDistrictSelect,
    onWardSelect,
    onSubmit
  } = useLocationForm(true);

  const {
    cityOptions,
    districtOptions,
    wardOptions,
    selectedCity,
    selectedDistrict,
    selectedWard
  } = state;

  return (
    <form>
      <div>
        <Select
          name="cityId"
          key={`cityId_${selectedCity?.value}`}
          isDisabled={cityOptions.length === 0}
          options={cityOptions}
          onChange={(option) => onCitySelect(option)}
          placeholder="Tỉnh/Thành"
          defaultValue={selectedCity}
        />

        <Select
          name="districtId"
          key={`districtId_${selectedDistrict?.value}`}
          isDisabled={districtOptions.length === 0}
          options={districtOptions}
          onChange={(option) => onDistrictSelect(option)}
          placeholder="Quận/Huyện"
          defaultValue={selectedDistrict}
        />

        <Select
          name="wardId"
          key={`wardId_${selectedWard?.value}`}
          isDisabled={wardOptions.length === 0}
          options={wardOptions}
          placeholder="Phường/Xã"
          onChange={(option) => onWardSelect(option)}
          defaultValue={selectedWard}
        />
      </div>
    </form>
  );
}

export default LocationForm;

Định nghĩa các hàm fetch data

Ngoài ra, ta cần hai function “ngoài luồng” nhưng cần phải xử lí bao gồm

  • Fetch data theo loại địa chỉ hành chính và mã.
  • Fetch bộ data tương ứng theo địa chỉ có sẵn khi đủ thông tin.

Tùy theo cách setup dữ liệu và cách thực thi để gọi API, bạn có thể define hai hàm trên phù hợp với project. Dưới đây là định nghĩa của mình trong demo:

// Fetch data theo địa chỉ hành chính và mã
function fetchLocationOptions(fetchType, paramId) {
    - Tạo url endpoint dựa theo fetchType và paramId.
    - Get response từ url endpoint.
    - Return sang định dạng option ({value, label}).
}
    // Fetch toàn bộ data từ data có sẵn
    function fetchInitialLocation() {
        - Lấy thông tin cityId, districtId, wardId.
        - Fetch các option city, district, ward.
        - Return state tương ứng cho custom hook.
    }
    

    Logic khi tải trang

    Việc tải trang như đã đề cập sẽ có hai trường hợp: tải mới trang và load danh sách Tỉnh/Thành hoặc tải từ một địa chỉ đủ thông tin có sẵn và các options tương ứng cho các select input, vì vậy việc viết useEffect cũng không quá phức tạp.

    // Logic khi first-load
    useEffect(() => {
        - Xác định trang là tải mới hay tải từ địa chỉ có sẵn.
        - Tải mới: load danh sách Tỉnh/Thành và update cityOptions.
        - Tải sẵn: load và update state fetchInitialLocation().
    }, []);
    

    Logic khi chọn Tỉnh/Thành

    Vì Tỉnh/Thành là đơn vị hành chính cấp cao nhất, sau khi chọn mới hoặc thay đổi option này, cần clear danh sách options cũng như selected option của Quận/Huyện và Phường/Xã hiện tại sau đó load danh sách option Quận/Huyện mới tương ứng.

    // Logic khi chọn một option city
    function onCitySelect(option) {
        - Cập nhật lại option mới cho selectedCity.
        - Clear danh sách và option Quận/Huyện, Phường/Xã.
        - Load thông tin Quận/Huyện từ Tỉnh/Thành đã chọn.
        - Update districtOptions từ thông tin đã load.
    }
    

    Logic khi chọn Quận/Huyện

    Tương tự với logic chọn Tỉnh/Thành, nhưng phần này chỉ cần xử lí logic cho đơn vị hành chính cấp dưới là Phường/Xã.

    // Logic khi chọn một option city
    function onDistrictSelect(option) {
        - Cập nhật lại option mới cho selectedDistrict.
        - Clear danh sách và option Phường/Xã.
        - Load thông tin Phường/Xã từ Quận/Huyện đã chọn.
        - Update districtOptions từ thông tin đã load.
    }
    

    Logic khi chọn Phường/Xã

    Đây là logic đơn giản nhất vì không có đơn vị hành chính con phụ thuộc, chỉ cần update lại selectedWard theo option đã chọn nên mình sẽ bỏ qua code phần này.


    Code cleanup một chút

    Nếu để ý một chút, trừ lúc first-load, ta dễ thấy việc fetch địa chỉ của Quận/Huyện chỉ xảy ra khi lựa chọn Tỉnh/Thành thay đổi, và việc fetch địa chỉ của Phường/Xã chỉ xảy ra khi lựa chọn Quận/Huyện thay đổi.

    Từ đó, ta có thể tách đoạn code xử lí load thông tin và gắn options mới cho địa chỉ hành chính cấp thấp hơn bằng useEffect thông qua thay đổi của selectedCity và selectedDistrict.

    useEffect(() => {
        - Check null của selectedCity, nếu null thì bỏ qua
        - Load thông tin Quận/Huyện từ Tỉnh/Thành đã chọn.
        - Update districtOptions từ thông tin đã load.
    }, [selectedCity])
    

    Thực hiện tương tự cho useEffect với dependency là selectedDistrict, lúc này phần code đã được chia nhỏ, dễ đọc và dễ bảo trì hơn.


    Tổng kết

    Hai phần bài viết đã mô tả chi tiết về cách mình thực hiện tạo 3 select input để chọn địa chỉ hành chính, bài toán điển hình cho việc các input phụ thuộc lẫn nhau và rất thông dụng trong thực tế, nếu bạn có cách tiếp cận hay cải tiến khác thì có thể thoải mái góp ý nhé =)

    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out /  Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out /  Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out /  Change )

    Connecting to %s