博客 / 詳情

返回

掌控 React 表單:詳解受控組件和非受控組件

大家好,我是長林啊!一個愛好 JavaScript、Go、Rust 的全棧開發者;致力於終生學習和技術分享。

本文首發在我的微信公眾號【長林啊】,歡迎大家關注、分享、點贊!

在開發過程中,經常涉及到用户輸入的表單處理;表單可以分為兩種類型:受控表單(Controlled Components)和非受控表單(Uncontrolled Components)。這兩種表單在處理用户輸入和狀態管理時有着不同的方式。例如 input 元素會根據輸入內容自動變化,而不是從組件中去獲取,這種不受 React 控制的表單元素我們定義為非受控組件。

我們先來創建一個 React 項目,用於下面相關概念的示例演示!下面就使用 vite 和 pnpm 來創建一個 React 應用。

$ pnpm create vite@latest controlled-vs-uncontrolled -- --template react

由於下面的示例會用到一些樣式,我們就來集成一下 tailwindcss

  • 安裝 tailwindcss

    $ pnpm add -D tailwindcss postcss autoprefixer
  • 初始化配置文件

    $ npx tailwindcss init -p

    命令執行完成之後,就會在項目的根目錄有一個 tailwind.config.js 文件和 postcss.config.js 文件,然後修改 content,完整內容如下:

    /** @type {import('tailwindcss').Config} */
    export default {
      content: [
        "./index.html",
        "./src/**/*.{js,ts,jsx,tsx}",
      ],
      theme: {
        extend: {},
      },
      plugins: [],
    }
  • 將 index.css 中的內容替換為下面的內容:

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    • base: 這個指定表示導入 tailwindcss 的基本樣式,裏面會包含一些預設樣式,主要目的是重置瀏覽器的樣式,重置了瀏覽器樣式之後,可以保證所有的瀏覽器中的外觀是一致的。
    • components:組件樣式,默認情況下沒有任何的組件樣式,後期我們可以在配置文件裏面自定義我們的組件樣式,以及使用第三方插件添加一些組件樣式,這一條指令是為了讓自定義組件樣式以及其他第三方組件樣式能夠生效。
    • utilities:這個指令就是導入實用的原子類。
  • 可以把原來的 App.css 文件刪除,因為我們使用 tailwindcss 就不需要腳手架自動生成的那些 css 文件了,當然也需要把 App.jsx 中引入的 App.css 刪掉。不刪掉可能會對我們後面的演示內容有一些影響。
  • 把App.jsx 中的DOM 內容替換,完整內容如下:

    import { useState } from 'react'
    
    function App () {
      const [count, setCount] = useState(0)
    
      return (
        <div>
          <h1 className="text-3xl font-bold underline">
            Hello world! {count}
          </h1>
          <button
            className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
            onClick={() => setCount(count + 1)}
          >
            Click me
          </button>
        </div>
      )
    }
    
    export default App
  • 在終端中執行 pnpm dev,在瀏覽器中訪問 localhost:5173,效果如下:
    <img src="https://files.mdnice.com/user/8213/40593189-d8c4-47b1-ad1d-214a4cf018f1.png" />

一、基礎

前面的環境準備好了,接下來就進入正題!

什麼是受控組件

受控組件就是由 React 來管理表單元素的值,同時表單元素的變化需要實時映射到 React 的 state 中,這個類似於雙向數據綁定。不同的表單元素,React 控制方式是不一樣的,如 inputvalue 來控制,checkboxchecked 來判斷是否選中等。

什麼是非受控組件

非受控組件(Uncontrolled Components)是指表單數據直接由 DOM 元素來管理,而不是通過 React 組件的 state 來管理。換句話説,非受控組件依賴於傳統的 HTML 表單處理方式,使用 ref 來直接訪問 DOM 元素並獲取其值。

二、受控組件

定義和特徵

  • 值由 React 狀態控制:表單元素的值由組件的狀態控制和管理。
  • 即時更新:每次用户輸入時,都會觸發狀態更新,從而更新表單元素的值。
  • 單一數據源:表單元素的值和組件的狀態保持一致,確保數據源的唯一性。

    受控組件的優劣

    優點

  • 更好的數據控制和驗證
  • 更容易實現複雜的交互邏輯

缺點

  • 代碼量較大
  • 需要更多的狀態管理

示例

下面就來下一個註冊頁面,其中包含文本框、單選框、複選框和下拉框的受控組件的應用,代碼如下:

import { useState } from 'react';

const hobby = ['Sports', 'Cooking', 'Traveling'];

const RegistrationForm = () => {
    const [form, setForm] = useState({
        nickname: '',
        age: '',
        password: '',
        sex: 'male', // 默認為male
        hobby: [],
        city: 'beijing',
    });

    const handleInputChange = (e) => {
        const { name, value, type, checked } = e.target;

        const newFormData = { ...form };
        const hobby = [...newFormData.hobby];
        const isCheckbox = type === 'checkbox';

        if (isCheckbox && !checked && hobby.includes(value)) {
            hobby.splice(hobby.indexOf(value), 1)
        } else hobby.push(value)
        newFormData[name] = isCheckbox ? hobby : value;

        setForm(newFormData);
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('Form submitted:', form);
        // 在這裏你可以添加提交表單的邏輯
    };

    return (
        <form onSubmit={handleSubmit} className="max-w-sm mx-auto">
            <div className="mb-6 w-full px-4">
                <label htmlFor="nickname" className="block mb-2">Nickname:</label>
                <input
                    type="text"
                    id="nickname"
                    name="nickname"
                    value={form.nickname}
                    onChange={handleInputChange}
                    className="w-full bg-transparent rounded-md border border-stroke dark:border-dark-3 ps-4 py-[10px] text-dark-6 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-gray-2"
                />
            </div>

            <div className="mb-6 w-full px-4">
                <label htmlFor="age" className="block mb-2">Age:</label>
                <input
                    type="number"
                    id="age"
                    name="age"
                    value={form.age}
                    onChange={handleInputChange}
                    className="w-full bg-transparent rounded-md border border-stroke dark:border-dark-3 ps-4 py-[10px] text-dark-6 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-gray-2"
                />
            </div>

            <div className="mb-6 w-full px-4">
                <label htmlFor="password" className="block mb-2">Password:</label>
                <input
                    type="password"
                    id="password"
                    name="password"
                    value={form.password}
                    onChange={handleInputChange}
                    className="w-full bg-transparent rounded-md border border-stroke dark:border-dark-3 ps-4 py-[10px] text-dark-6 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-gray-2"
                />
            </div>

            <div className="mb-6 w-full px-4">
                <label className="block mb-2">Sex:</label>
                <label className="inline-flex items-center mr-4">
                    <input
                        type="radio"
                        name="sex"
                        value="male"
                        checked={form.sex === 'male'}
                        onChange={handleInputChange}
                    />
                    <span className="ml-2">Male</span>
                </label>
                <label className="inline-flex items-center">
                    <input
                        type="radio"
                        name="sex"
                        value="female"
                        checked={form.sex === 'female'}
                        onChange={handleInputChange}
                    />
                    <span className="ml-2">Female</span>
                </label>
            </div>

            <div className="mb-6 w-full px-4">
                <fieldset>
                    <legend className="block mb-2">Hobby:</legend>
                    {hobby.map(item => (<label className="inline-flex items-center mr-4" key={item}>
                        <input
                            type="checkbox"
                            name='hobby'
                            value={item}
                            checked={form.hobby.includes(item)}
                            onChange={handleInputChange}
                        />
                        <span className="ml-2">{item}</span>
                    </label>))}

                </fieldset>
            </div>

            <div className="mb-6 w-full px-4">
                <label htmlFor="city" className="block mb-2">City:</label>
                <select
                    id="city"
                    name="city"
                    value={form.city}
                    onChange={handleInputChange}
                    className="w-full bg-transparent rounded-md border border-stroke dark:border-dark-3 ps-4 py-[10px] text-dark-6 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-gray-2"
                >
                    <option value="beijing">Beijing</option>
                    <option value="shanghai">Shanghai</option>
                    <option value="guangzhou">Guangzhou</option>
                    <option value="sichuan">Sichuan</option>
                </select>
            </div>

            <button
                type="submit"
                className="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-700"
            >
                Register
            </button>
        </form>
    );
};

export default RegistrationForm;

效果如下:
<img src="https://files.mdnice.com/user/8213/00535ab5-618e-44f3-8270-8596a7c9936f.png" />

上面的例子通過 useState 鈎子定義和初始化表單的狀態對象 form,其中包含字段:nicknameagepasswordsexhobbycity

  • 暱稱、年齡、密碼:這些文本和數字輸入框的值通過 value 屬性與狀態綁定,通過 onChange 事件處理函數更新狀態。
  • 性別:單選按鈕組的選中狀態通過 checked 屬性與狀態綁定,通過 onChange 事件處理函數更新狀態。
  • 興趣愛好:複選框的選中狀態通過 checked 屬性與狀態綁定,通過 onChange 事件處理函數更新狀態。函數會根據複選框的選中與否更新狀態中的 hobby 數組。
  • 城市:下拉選擇框的值通過 value 屬性與狀態綁定,通過 onChange 事件處理函數更新狀態。

三、非受控組件

特點

  • 表單數據不受 React 控制:非受控組件中的表單元素值並不保存在 React 的 state 中,而是直接從 DOM 元素中獲取。
  • 使用 ref 訪問 DOM:通過 ref 可以直接引用 DOM 元素並讀取其值。

    優點

  • 更少的代碼量:由於不需要維護 state,非受控組件的代碼量較少。
  • 狀態管理簡單:無需通過事件處理函數來更新 state,狀態管理相對簡單。

    缺點

  • 數據同步較難:因為表單數據不保存在state中,數據同步和驗證相對較難。
  • 處理複雜交互邏輯不便:在處理複雜的表單交互邏輯時,非受控組件較為不便。

示例

把上面的受控組件改成非受控組件,完整代碼如下:

const hobby = ['Sports', 'Cooking', 'Traveling'];

const RegistrationFormUncontrolled = () => {
    // 處理表單提交
    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('Form submitted:', e);
        const nickname = e.target.nickname.value;
        const age = e.target.age.value;
        const sex = e.target.sex.value;
        const password = e.target.password.value;
        const hobby = e.target.hobby.value;
        const city = e.target.city.value;
        console.log('🚀 ~ handleSubmit ~ formData:', nickname, age, sex, password, hobby, city)
        // 在這裏你可以添加提交表單的邏輯
    };

    return (
        <form onSubmit={handleSubmit} className="max-w-sm mx-auto">
            <div className="mb-6 w-full px-4">
                <label htmlFor="nickname" className="block mb-2">Nickname:</label>
                <input
                    type="text"
                    id="nickname"
                    name="nickname"
                    className="w-full bg-transparent rounded-md border border-stroke dark:border-dark-3 ps-4 py-[10px] text-dark-6 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-gray-2"
                />
            </div>

            <div className="mb-6 w-full px-4">
                <label htmlFor="age" className="block mb-2">Age:</label>
                <input
                    type="number"
                    id="age"
                    name="age"
                    className="w-full bg-transparent rounded-md border border-stroke dark:border-dark-3 ps-4 py-[10px] text-dark-6 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-gray-2"
                />
            </div>

            <div className="mb-6 w-full px-4">
                <label htmlFor="password" className="block mb-2">Password:</label>
                <input
                    type="password"
                    id="password"
                    name="password"
                    className="w-full bg-transparent rounded-md border border-stroke dark:border-dark-3 ps-4 py-[10px] text-dark-6 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-gray-2"
                />
            </div>

            <div className="mb-6 w-full px-4">
                <label className="block mb-2">Sex:</label>
                <label className="inline-flex items-center mr-4">
                    <input
                        type="radio"
                        name="sex"
                        value="male"
                    />
                    <span className="ml-2">Male</span>
                </label>
                <label className="inline-flex items-center">
                    <input
                        type="radio"
                        name="sex"
                        value="female"
                    />
                    <span className="ml-2">Female</span>
                </label>
            </div>

            <div className="mb-6 w-full px-4">
                <fieldset>
                    <legend className="block mb-2">Hobby:</legend>
                    {hobby.map(item => (<label className="inline-flex items-center mr-4" key={item}>
                        <input
                            type="checkbox"
                            name='hobby'
                            value={item}
                        />
                        <span className="ml-2">{item}</span>
                    </label>))}

                </fieldset>
            </div>

            <div className="mb-6 w-full px-4">
                <label htmlFor="city" className="block mb-2">City:</label>
                <select
                    id="city"
                    name="city"
                    className="w-full bg-transparent rounded-md border border-stroke dark:border-dark-3 ps-4 py-[10px] text-dark-6 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-gray-2"
                >
                    <option value="beijing">Beijing</option>
                    <option value="shanghai">Shanghai</option>
                    <option value="guangzhou">Guangzhou</option>
                    <option value="sichuan">Sichuan</option>
                </select>
            </div>

            <button
                type="submit"
                className="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-700"
            >
                Register
            </button>
        </form>
    );
};

export default RegistrationFormUncontrolled;

效果也是一樣的:

QQ_1721729411124

不過獲取數據的方式不太一樣,使用非受控表單獲取數據的幾種方法:

  • 使用 ref 獲取表單數據

    import React, { useRef } from 'react';
    
    function UncontrolledForm() {
      const nameRef = useRef(null);
      const emailRef = useRef(null);
    
      const handleSubmit = (e) => {
        e.preventDefault();
        const name = nameRef.current.value;
        const email = emailRef.current.value;
        alert(`Name: ${name}, Email: ${email}`);
      };
    
      return (
        <form onSubmit={handleSubmit}>
          <div>
            <label>
              Name:
              <input type="text" ref={nameRef} />
            </label>
          </div>
          <div>
            <label>
              Email:
              <input type="email" ref={emailRef} />
            </label>
          </div>
          <button type="submit">Submit</button>
        </form>
      );
    }
    
    export default UncontrolledForm;
  • 直接訪問 event.target 獲取表單數據

    const handleSubmit = (e) => {
        e.preventDefault();
        const name = e.target.name.value;
        const email = e.target.email.value;
        alert(`Name: ${name}, Email: ${email}`);
    };
  • 使用 FormData 獲取表單數據

    const handleSubmit = (e) => {
        e.preventDefault();
        const formData = new FormData(e.target);
        const name = formData.get('name');
        const email = formData.get('email');
        alert(`Name: ${name}, Email: ${email}`);
    };

    四、受控組件與非受控組件的對比

    使用場景

    受控組件適用場景

  • 表單處理

    • 實時驗證:可以在用户輸入時立即進行驗證(如字符長度、格式);例如,驗證電子郵件格式、密碼強度等。
    • 動態表單:根據用户輸入動態調整表單內容,如顯示/隱藏額外的輸入字段。 例如,選擇某個選項後顯示額外的詳細信息輸入框。
    • 多步驟表單:管理多步驟表單的狀態和步驟之間的數據傳遞。例如,分步註冊表單,逐步收集用户信息。
  • 統一狀態管理

    • 狀態同步:輸入狀態與組件狀態保持同步,確保數據的一致性。例如,相同的輸入數據在多個組件間共享。
    • 可預測性:狀態變化可控,便於追蹤和調試。例如,調試用户輸入導致的狀態變化問題。
  • 複雜交互

    • 動態內容更新:根據用户輸入實時更新顯示內容。例如,搜索框實時顯示搜索結果。
    • 用户反饋:根據輸入情況給用户即時反饋,如錯誤提示、成功消息等。例如,輸入有效時顯示綠色勾選圖標,無效時顯示紅色叉號。
  • 一致的用户體驗

    • 統一樣式:通過狀態管理和樣式綁定,確保所有輸入元素的樣式一致。例如,所有輸入框獲得焦點時邊框顏色一致。
    • 行為一致:通過狀態控制確保所有輸入元素的交互行為一致。例如,所有輸入框在失去焦點時進行驗證。
  • 數據綁定

    • API數據綁定:將用户輸入的數據與後端API數據進行綁定和同步。例如,用户輸入地址信息時實時查詢和顯示地址建議。
    • 本地存儲同步:將用户輸入的數據與本地存儲(如localStorage)進行同步,保持數據持久性。例如,表單數據自動保存到本地存儲,防止頁面刷新導致的數據丟失。

非受控組件適用場景

  1. 簡單表單;對於簡單表單,不需要複雜狀態管理的場景,可以使用非受控組件。

    • 快速實現:適用於簡單的表單或輸入元素,不需要實時驗證或動態更新。例如,單個輸入框或簡單的留言表單。
    • 低頻交互:適用於用户交互頻率較低的表單。例如,靜態內容提交或一次性表單提交。
  2. 第三方庫集成;在使用某些第三方庫時,非受控組件可能更適合,因為這些庫可能會直接操作DOM節點。

    • 文件上傳:使用非受控組件處理文件上傳,因為文件輸入通常直接與DOM節點交互。例如,使用<input type="file">進行文件選擇。
    • 繪圖或富文本編輯:第三方繪圖庫或富文本編輯器可能需要直接訪問DOM節點。例如,集成Quill富文本編輯器或Canvas繪圖庫。
  3. 性能優化;在某些情況下,使用非受控組件可以減少不必要的重渲染,提高性能。

    • 避免過多狀態更新:對於高頻輸入場景,非受控組件可以減少狀態更新次數和組件重渲染。例如,實時輸入大文本或快速輸入場景。
    • 減少渲染開銷:避免由於狀態變更導致的頻繁渲染,提升性能。例如,處理較大數據或複雜表單時。
  4. 簡化代碼,對於一些場景,使用非受控組件可以簡化代碼,降低複雜度。

    • 減少狀態管理:不需要維護複雜的狀態管理邏輯,表單數據直接從DOM中獲取。例如,簡單的搜索框或單個輸入框。
    • 簡潔代碼:代碼更加簡潔明瞭,減少不必要的狀態和事件處理。例如,快速創建簡單的輸入表單。

五、最佳實踐

選擇合適的組件類型

在項目中選擇使用受控組件還是非受控組件可以根據以下幾個關鍵因素進行考慮:

關鍵因素 受控組件適用場景 非受控組件適用場景
表單複雜性 複雜表單:需要處理許多交互和驗證邏輯
實時驗證:需要實時驗證用户輸入
簡單表單:輸入字段較少且不需要複雜的狀態管理
低頻交互:不需要實時反饋和驗證
性能考慮 高頻交互:可能導致過多狀態更新,需優化
可預測性:狀態變化可控,便於調試
避免過多狀態更新:減少不必要的狀態更新
減少渲染開銷:直接操作DOM節點
第三方庫集成 受限:某些第三方庫可能不適合與受控組件配合使用 靈活性:適合需要直接操作DOM節點的第三方庫,如文件上傳、富文本編輯器
數據初始化 預填充數據:組件掛載時預填充數據,並實時更新狀態 延遲初始化:組件掛載後從外部源獲取初始數據並填充表單
代碼簡潔性 全面控制:提供對錶單和輸入狀態的全面控制,但需編寫較多狀態管理和事件處理代碼 簡潔實現:適用於不需要複雜狀態管理的場景,代碼實現簡潔明瞭

何時以及如何混合使用受控和非受控組件

混合使用受控和非受控組件可以在特定場景下帶來最佳的用户體驗和性能優化。

何時混合使用:

  • 性能優化:在高頻交互的場景下,某些輸入字段使用非受控組件以減少不必要的狀態更新,而其他需要嚴格控制和驗證的字段使用受控組件。
  • 複雜表單:對於包含多個輸入字段的複雜表單,部分字段需要實時驗證和反饋(使用受控組件),而其他字段可以直接與DOM交互(使用非受控組件)。
  • 第三方庫集成:當使用需要直接操作DOM的第三方庫(如文件上傳、富文本編輯器)時,可以將這些部分作為非受控組件,而將其他部分作為受控組件。

混合使用

  • 高頻輸入和實時驗證

    import { useState, useRef } from 'react';
    
    function MixedForm () {
        const [name, setName] = useState('');
        const passwordRef = useRef(null);
    
        const handleNameChange = (e) => {
            setName(e.target.value);
        };
    
        const handleSubmit = (e) => {
            e.preventDefault();
            const email = passwordRef.current.value;
            alert(`Name: ${name}, Email: ${email}`);
        };
    
        return (
            <div className="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
                <div className="sm:mx-auto sm:w-full sm:max-w-sm">
                    <img className="mx-auto h-10 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company" />
                    <h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Sign in to your account</h2>
                </div>
    
                <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
                    <form className="space-y-6" action="#" method="POST" onSubmit={handleSubmit}>
                        <div>
                            <label htmlFor="email" className="block text-sm font-medium leading-6 text-gray-900">Email address</label>
                            <div className="mt-2">
                                {/* 受控組件 */}
                                <input id="email" name="email" type="email" autoComplete="email" required className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" onChange={handleNameChange} />
                            </div>
                        </div>
    
                        <div>
                            <div className="flex items-center justify-between">
                                <label htmlFor="password" className="block text-sm font-medium leading-6 text-gray-900">Password</label>
                                <div className="text-sm">
                                    <a href="#" className="font-semibold text-indigo-600 hover:text-indigo-500">Forgot password?</a>
                                </div>
                            </div>
                            <div className="mt-2">
                                {/* 非受控組件 */}
                                <input ref={passwordRef} id="password" name="password" type="password" autoComplete="current-password" required className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" />
                            </div>
                        </div>
    
                        <div>
                            <button type="submit" className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign in</button>
                        </div>
                    </form>
    
                    <p className="mt-10 text-center text-sm text-gray-500">
                        Not a member?
                        <a href="#" className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Start a 14 day free trial</a>
                    </p>
                </div>
            </div>
        );
    }
    
    export default MixedForm;
  • 文件上傳和其他輸入

    import { useState, useRef } from 'react';
    
    function FileUploadForm () {
        const [username, setUsername] = useState('');
        const fileInputRef = useRef(null);
    
        const handleUsernameChange = (e) => {
            setUsername(e.target.value);
        };
    
        const handleSubmit = (e) => {
            e.preventDefault();
            const file = fileInputRef.current.files[0];
            alert(`Username: ${username}, File: ${file ? file.name : 'No file selected'}`);
        };
    
        return (
            <form onSubmit={handleSubmit}>
                {/* 受控組件 */}
                <div className="w-full px-4 md:w-1/2 lg:w-1/3">
                    <div className="mb-12">
                        <label htmlFor="" className="mb-[10px] block text-base font-medium text-dark dark:text-white">
                            受控組件
                        </label>
                        <input type="text" placeholder="entry your username" value={username} onChange={handleUsernameChange} className="w-full bg-transparent rounded-md border border-stroke dark:border-dark-3 py-[10px] px-5 text-dark-6 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-gray-2 disabled:border-gray-2" />
                    </div>
                </div>
    
                {/* 非受控組件 */}
                <div className="w-full px-4 md:w-1/2 lg:w-1/3">
                    <div className="mb-12">
                        <label className="mb-[10px] block text-base font-medium text-dark dark:text-white">
                            非受控組件
                        </label>
                        <div className="relative">
                            <label htmlFor="file" className="flex min-h-[175px] w-full cursor-pointer items-center justify-center rounded-md border border-dashed border-primary p-6">
                                <div>
                                    <input type="file" name="file" id="file" ref={fileInputRef} className="sr-only" />
                                    <span className="mx-auto mb-3 flex h-[50px] w-[50px] items-center justify-center rounded-full border border-stroke dark:border-dark-3 bg-white dark:bg-dark-2">
                                        <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                                            <path fillRule="evenodd" clipRule="evenodd" d="M2.5013 11.666C2.96154 11.666 3.33464 12.0391 3.33464 12.4993V15.8327C3.33464 16.0537 3.42243 16.2657 3.57871 16.4219C3.73499 16.5782 3.94695 16.666 4.16797 16.666H15.8346C16.0556 16.666 16.2676 16.5782 16.4239 16.4219C16.5802 16.2657 16.668 16.0537 16.668 15.8327V12.4993C16.668 12.0391 17.0411 11.666 17.5013 11.666C17.9615 11.666 18.3346 12.0391 18.3346 12.4993V15.8327C18.3346 16.4957 18.0712 17.1316 17.6024 17.6004C17.1336 18.0693 16.4977 18.3327 15.8346 18.3327H4.16797C3.50493 18.3327 2.86904 18.0693 2.4002 17.6004C1.93136 17.1316 1.66797 16.4957 1.66797 15.8327V12.4993C1.66797 12.0391 2.04106 11.666 2.5013 11.666Z" fill="#3056D3"></path>
                                            <path fillRule="evenodd" clipRule="evenodd" d="M9.41074 1.91009C9.73618 1.58466 10.2638 1.58466 10.5893 1.91009L14.7559 6.07676C15.0814 6.4022 15.0814 6.92984 14.7559 7.25527C14.4305 7.58071 13.9028 7.58071 13.5774 7.25527L10 3.67786L6.42259 7.25527C6.09715 7.58071 5.56951 7.58071 5.24408 7.25527C4.91864 6.92984 4.91864 6.4022 5.24408 6.07676L9.41074 1.91009Z" fill="#3056D3"></path>
                                            <path fillRule="evenodd" clipRule="evenodd" d="M10.0013 1.66602C10.4615 1.66602 10.8346 2.03911 10.8346 2.49935V12.4994C10.8346 12.9596 10.4615 13.3327 10.0013 13.3327C9.54106 13.3327 9.16797 12.9596 9.16797 12.4994V2.49935C9.16797 2.03911 9.54106 1.66602 10.0013 1.66602Z" fill="#3056D3"></path>
                                        </svg>
                                    </span>
                                    <span className="text-base text-body-color dark:text-dark-6">
                                        Drag &amp; drop or
                                        <span className="text-primary underline"> browse </span>
                                    </span>
                                </div>
                            </label>
                        </div>
                    </div>
                </div>
    
                <button type="submit" className="border-dark dark:border-dark-2 border rounded-md inline-flex items-center justify-center py-3 px-7 text-center text-base font-medium text-dark dark:text-white hover:bg-gray-4 dark:hover:bg-dark-3 disabled:bg-gray-3 disabled:border-gray-3 disabled:text-dark-5">Submit</button>
            </form>
        );
    }
    
    export default FileUploadForm;

    狀態管理

    在使用受控組件時,高效的狀態管理可以提升應用的性能和代碼的可讀性。以下是一些最佳實踐和策略,可以幫助你在React應用中更高效地管理狀態:

  • 使用React Hooks

React Hooks(例如 useState 和 useReducer)是管理組件狀態的核心工具。

  • useState

    適用於簡單的狀態管理,例如單個表單輸入字段。

    import React, { useState } from 'react';
    
    function SimpleForm() {
      const [name, setName] = useState('');
    
      const handleNameChange = (e) => {
        setName(e.target.value);
      };
    
      return (
        <form>
          <input type="text" value={name} onChange={handleNameChange} />
        </form>
      );
    }
  • useReducer

    適用於更復雜的狀態管理,例如包含多個字段的表單或需要處理多個狀態更新邏輯。

    import React, { useReducer } from 'react';
    
    const initialState = { name: '', email: '' };
    
    function reducer(state, action) {
      switch (action.type) {
        case 'SET_NAME':
          return { ...state, name: action.payload };
        case 'SET_EMAIL':
          return { ...state, email: action.payload };
        default:
          return state;
      }
    }
    
    function ComplexForm() {
      const [state, dispatch] = useReducer(reducer, initialState);
    
      const handleNameChange = (e) => {
        dispatch({ type: 'SET_NAME', payload: e.target.value });
      };
    
      const handleEmailChange = (e) => {
        dispatch({ type: 'SET_EMAIL', payload: e.target.value });
      };
    
      return (
        <form>
          <input type="text" value={state.name} onChange={handleNameChange} />
          <input type="email" value={state.email} onChange={handleEmailChange} />
        </form>
      );
    }
  1. 將狀態提升

    將狀態提升到最近的公共祖先組件中,以便多個子組件共享和同步狀態。

    import React, { useState } from 'react';
    
    function ParentComponent() {
      const [formData, setFormData] = useState({ name: '', email: '' });
    
      const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData((prevData) => ({ ...prevData, [name]: value }));
      };
    
      return (
        <>
          <ChildComponent1 formData={formData} handleChange={handleChange} />
          <ChildComponent2 formData={formData} handleChange={handleChange} />
        </>
      );
    }
    
    function ChildComponent1({ formData, handleChange }) {
      return (
        <input type="text" name="name" value={formData.name} onChange={handleChange} />
      );
    }
    
    function ChildComponent2({ formData, handleChange }) {
      return (
        <input type="email" name="email" value={formData.email} onChange={handleChange} />
      );
    }
  2. 使用自定義Hooks

    自定義Hooks可以封裝和重用複雜的狀態邏輯。

    import React, { useState } from 'react';
    
    function useForm(initialState) {
      const [formData, setFormData] = useState(initialState);
    
      const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData((prevData) => ({ ...prevData, [name]: value }));
      };
    
      return [formData, handleChange];
    }
    
    function MyForm() {
      const [formData, handleChange] = useForm({ name: '', email: '' });
    
      return (
        <form>
          <input type="text" name="name" value={formData.name} onChange={handleChange} />
          <input type="email" name="email" value={formData.email} onChange={handleChange} />
        </form>
      );
    }
  3. 使用狀態管理庫

對於大型應用,可以考慮使用狀態管理庫,如 Redux 或 Zustand。

社區的 React form 方案

  • react-hook-form
  • formik
  • react-jsonschema-form
  • @tanstack/react-form
  • @formily/react
  • redux-form
user avatar laughingzhu 頭像 esunr 頭像 coderleo 頭像 gaoming13 頭像 waweb 頭像 pengxiaohei 頭像 nihaojob 頭像
7 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.