iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0
Modern Web

30天React練功坊-攻克常見實務/面試問題系列 第 20

30天React練功坊-攻克常見實務/面試問題 Day20: useState with complex form

  • 分享至 

  • xImage
  •  
tags: ItIron2023 react

前言

我們昨天看了一個有趣的useState問題,了解到initialValue的一些限制,今天我們會再看一個與state管理有關的例子,而它會稍稍有些複雜,還請見諒!我們馬上開始吧!

本日題目

請觀察一下這個codesandbox以及下方的截圖。

day20-demo-image

今天你設計了一個表單,這個表單稍稍有些複雜,其中部分的欄位彼此相關,舉個例子來說你需要填寫Current Job的欄位後,Next Job這個可選填的欄位才會開啟,同時僅有當Name, Degree & Current Job這些必填欄位都填寫後,送出按鈕才可以點擊。

我們之前有個例子是利用一個大物件做狀態管理,這樣你就不用寫一堆state,你也吸取了上次的經驗完成了這次的需求,功能上並沒有任何問題,唯一的小小問題就是在於由於欄位之間有一些依賴關係,在你每一次更新state的同時都需要透過一個useEffect去檢查是否應該開啟Next Job以及Submit,在管理上就會稍微困難一些,請觀察以下的程式碼並試著優化相關的邏輯。

import React, { useState, useEffect } from "react";

function ComplexForm() {
  const [state, setState] = useState({
    personalInfo: { name: "", age: 0 },
    education: { degree: "", isValid: false },
    employment: { currentJob: "", nextJob: "", isValid: false },
    isSubmitEnabled: false
  });

  useEffect(() => {
    const { personalInfo, education, employment } = state;
    const newIsSubmitEnabled =
      personalInfo.name && education.isValid && employment.isValid;

    if (state.isSubmitEnabled !== newIsSubmitEnabled) {
      setState((prevState) => ({
        ...prevState,
        isSubmitEnabled: newIsSubmitEnabled
      }));
    }
  }, [state]);

  const updatePersonalInfo = (name, age) => {
    setState((prevState) => ({
      ...prevState,
      personalInfo: { name, age }
    }));
  };

  const updateEducation = (degree, isValid) => {
    setState((prevState) => ({
      ...prevState,
      education: { degree, isValid }
    }));
  };

  const updateEmployment = (currentJob, nextJob, isValid) => {
    setState((prevState) => ({
      ...prevState,
      employment: { currentJob, nextJob, isValid }
    }));
  };

  return (
    <div>
      <h1>useState with complex form</h1>
      <div>
        <label>Name: </label>
        <input
          type="text"
          onChange={(e) =>
            updatePersonalInfo(e.target.value, state.personalInfo.age)
          }
        />
      </div>
      <div>
        <label>Degree: </label>
        <select
          onChange={(e) =>
            updateEducation(e.target.value, e.target.value !== "")
          }
        >
          <option value="">Select</option>
          <option value="Bachelors">Bachelors</option>
          <option value="Masters">Masters</option>
        </select>
      </div>
      <div>
        <label>Current Job: </label>
        <input
          type="text"
          onChange={(e) =>
            updateEmployment(
              e.target.value,
              state.employment.nextJob,
              e.target.value !== ""
            )
          }
        />
      </div>
      <div>
        <label>Next Job: </label>
        <input
          type="text"
          disabled={!state.employment.currentJob}
          onChange={(e) =>
            updateEmployment(
              state.employment.currentJob,
              e.target.value,
              e.target.value !== ""
            )
          }
        />
      </div>
      <button disabled={!state.isSubmitEnabled}>Submit</button>
    </div>
  );
}

export default ComplexForm;

解答與基本解釋

這個題目有著不止一種以上的解法,其中完全不要去更動其實也是一種選擇,畢竟它現階段並沒有真的造成什麼大問題,不過往往這類有相依關係的複雜state,光靠一個useState並配合物件作為initialValue會稍嫌有些吃力,畢竟你每一次的更動都要去考慮相依的值,不但在程式碼上需要額外的處理,最重要的是往往會讓你的程式碼可讀性變低,比方說像是這次範例中的useEffect就會讓人看起來有些許困惑。這類的情況一般來說我會建議採用useReducer,一個特別適用於複雜state管理的hook,尤其在state間有彼此相依的情況出現時。老樣子,如果你沒有聽過這玩意,我最後會留連結讓你去補課,我這邊只會跟你說為什麼我會採用這個解法以及我會怎麼用,上方的程式碼用useReducer改寫後會變為類似這樣。

基本上我們建立了一模一樣的初始state值,差別在於我們給予每一個操作對應的action,讓你的程式碼看起來稍稍乾淨一點,同時我們將validate的邏輯放在每一次的state更新中處理而不是另外讓一個useEffect來幫忙!一旦有這樣的結構出現,後續你需要修改或增加新的邏輯處理往往會方便不少!這麼一來未來要做維護也會輕鬆一些!

const initialState = {
  personalInfo: { name: '', age: 0 },
  education: { degree: '', isValid: false },
  employment: { currentJob: '', nextJob: '', isValid: false },
  isSubmitEnabled: false,
};

function formReducer(state, action) {
  let updatedState = { ...state };

  switch (action.type) {
    case 'UPDATE_PERSONAL_INFO':
      updatedState.personalInfo = action.payload;
      break;
    case 'UPDATE_EDUCATION':
      updatedState.education = action.payload;
      break;
    case 'UPDATE_EMPLOYMENT':
      updatedState.employment = action.payload;
      break;
    default:
      return state;
  }

  // Run the validation logic every time state updates
  const { personalInfo, education, employment } = updatedState;
  updatedState.isSubmitEnabled = personalInfo.name && education.isValid && employment.isValid;

  return updatedState;
}

function ComplexForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);

  const updatePersonalInfo = (name, age) => {
    dispatch({
      type: 'UPDATE_PERSONAL_INFO',
      payload: { name, age },
    });
  };

  const updateEducation = (degree, isValid) => {
    dispatch({
      type: 'UPDATE_EDUCATION',
      payload: { degree, isValid },
    });
  };

  const updateEmployment = (currentJob, nextJob, isValid) => {
    dispatch({
      type: 'UPDATE_EMPLOYMENT',
      payload: { currentJob, nextJob, isValid },
    });
  };

  return (
    <div>
      <h1>Complex Form</h1>
      <div>
        <label>Name: </label>
        <input
          type="text"
          onChange={(e) => updatePersonalInfo(e.target.value, state.personalInfo.age)}
        />
      </div>
      <div>
        <label>Degree: </label>
        <select
          onChange={(e) => updateEducation(e.target.value, e.target.value !== '')}
        >
          <option value="">Select</option>
          <option value="Bachelors">Bachelors</option>
          <option value="Masters">Masters</option>
        </select>
      </div>
      <div>
        <label>Current Job: </label>
        <input
          type="text"
          onChange={(e) => updateEmployment(e.target.value, state.employment.nextJob, e.target.value !== '')}
        />
      </div>
      <div>
        <label>Next Job: </label>
        <input
          type="text"
          disabled={!state.employment.currentJob}
          onChange={(e) => updateEmployment(state.employment.currentJob, e.target.value, e.target.value !== '')}
        />
      </div>
      <button disabled={!state.isSubmitEnabled}>Submit</button>
    </div>
  );
}

export default ComplexForm;

總結

我們今天看了一個useState v.s useReducer的情境,坦白說這一直都是個許多人爭吵的議題,在絕大多數的情境下useState就足以解決了,尤其當你用useState管理一個大物件時,許多人甚至說不出這與useReducer差在哪邊。不過就像之前在講useLayoutEffect時提過的,只要你能認清你要處理的情況,有些工具就確實能好好的幫上忙。useReducer在處理複雜的state管理時確實相當出色,也經常配合context去使用,如果你對這玩意不熟悉的話我建議你稍微補一下官網的課,相信我他總有一天會幫上忙的!我們明天見囉,終於要進入面試情境題了!

本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!


上一篇
30天React練功坊-攻克常見實務/面試問題 Day19: useState initial value not update after re-render
下一篇
30天React練功坊-攻克常見實務/面試問題 Day21: React render logic interview question
系列文
30天React練功坊-攻克常見實務/面試問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言