首頁  >  問答  >  主體

React 替代的支撐鑽探方式(反向,從子級到父級)處理表單

<p>我是 React 新手,透過一些實作專案來學習它。我目前正在從事表單處理和驗證工作。我在 SPA 中使用 React Router 的 Form 元件,並且在表單中我有 FormGroup 元素,它呈現標籤輸入和錯誤訊息。我還在 FormGroup 元件中使用自己的輸入元件來分離表單中使用的輸入的邏輯和狀態管理。 </p> <p>因此,我將 Form 元件和 FormGroup 元件放置在範例登入頁面中,如下所示:</p> <p><em>pages/Login.js</em></p> <pre class="brush:js;toolbar:false;">import { useState } 從 'react'; import { Link, Form, useNavigate, useSubmit } 從 'react-router-dom'; import FormGroup from '../components/UI/FormGroup'; import Button from '../components/UI/Button'; import Card from '../components/UI/Card'; import './Login.scss'; function LoginPage() { const navigate = useNavigate(); const submit = useSubmit(); const [isLoginValid, setIsLoginValid] = useState(false); const [isPasswordValid, setIsPasswordValid] = useState(false); var resetLoginInput = null; var resetPasswordInput = null; let isFormValid = false; if(isLoginValid && isPasswordValid) { isFormValid = true; } function formSubmitHandler(event) { event.preventDefault(); if(!isFormValid) { return; } resetLoginInput(); resetPasswordInput(); submit(event.currentTarget); } function loginValidityChangeHandler(isValid) { setIsLoginValid(isValid); } function passwordValidityChangeHandler(isValid) { setIsPasswordValid(isValid); } function resetLoginInputHandler(reset) { resetLoginInput = reset; } function resetPasswordInputHandler(reset) { resetPasswordInput = reset; } function switchToSignupHandler() { navigate('/signup'); } return ( <div className="login"> <div className="login__logo"> Go Cup </div> <p className="login__description"> 登入您的 Go Cup 帳戶 </p> <卡片邊框> <表單onSubmit={formSubmitHandler}> <表單組 id=“登入” label="使用者名稱或電子郵件地址" 輸入屬性={{ 類型:“文字”, 名稱:“登入”, 有效性:(值)=> { 值 = value.trim(); 如果(!值){ return [false, '需要使用者名稱或電子郵件地址。'] } else if(value.length < 3 || value.length > 30) { return [false, '使用者名稱或電子郵件地址必須至少有 3 個字符,最多 30 個字符']; } 別的 { 返回[真,空]; } }, onValidityChange:loginValidityChangeHandler, onReset:重置LoginInputHandler }} >> <表單組 id=“密碼” 標籤=“密碼” sideLabelElement={ <連結到=“/密碼重設”> 忘記密碼? } 輸入屬性={{ 輸入:“密碼”, 名稱:“密碼”, 有效性:(值)=> { 值 = value.trim(); 如果(!值){ return [false, '需要密碼。'] } else if(value.length < 4 || value.length > 1024) { return [false, '密碼長度必須至少為 4 個或最多 1024 個字元。']; } 別的 { 返回[真,空]; } }, onValidityChange:passwordValidityChangeHandler, onReset:重設密碼輸入處理程序 }}>>
<按鈕類別名稱=“w-100”類型=“提交”> 登入 </按鈕> 或者 </span> <按鈕類別名稱=“w-100” onClick={switchToSignupHandler}> 報名 </按鈕>
</表格> </卡>
; ); } export default LoginPage; </pre> <p>如您在上面的程式碼中所看到的,我使用FormGroup 元件並傳遞<code>onValidityChange</code> 和<code>onReset</code> 屬性來取得<code>isValid< code> 值的更新值。表單提交後重置輸入的變更和重置函數等。使用我的自訂掛鉤 useInput 在輸入元件中建立 <code>isValid</code> 和 <code>reset</code> 函數。我在值變更時傳遞 isValid 值,並使用 FormGroup 元件中定義的 props 從輸入元件傳遞重設函數。我還在登入頁面中使用 <code>isLoginValid</code> 和 <code>isPasswordValid</code> states defiend 來儲存從子輸入元件傳遞的更新的 <code>isValid</code> 狀態值。因此,我已經在輸入元件中定義了狀態,並使用 props 將它們傳遞給父元件,並將它們的值儲存在該父元件中建立的其他狀態中。正在進行的道具鑽孔讓我感覺有點不舒服。 </p> <p>狀態是在輸入元件內部管理的,我有這些狀態:</p> <ul> <li><strong>值:</strong>輸入元素的值。 </li> <li><strong>isInputTouched</strong>:確定使用者是否已觸摸/聚焦輸入,以確定是否顯示驗證錯誤訊息(如果有)。 </li> </ul> <p>我將一些函數(例如傳遞給輸入組件的驗證函數)組合併應用到這兩個狀態,以創建其他變數值來收集有關輸入及其有效性的信息,例如該值是否有效(isValid )、是否有訊息驗證(訊息),如果輸入有效(<code>isInputValid = isValid || !isInputTouched</code>)來決定顯示驗證訊息。</p> <p>這些狀態和值在我建立的自訂掛鉤 <code>useInput</code> 中管理,如下所示:</p> <p><em>hooks/use-state.js</em></p> <pre class="brush:js;toolbar:false;">import { useState, useCallback } 從 'react'; function useInput(validityFn) { const [value, setValue] = useState(''); const [isInputTouched, setIsInputTouched] = useState(false); const [isValid, message] = typeof validityFn === 'function' ? validityFn(value) : [true, null]; const isInputValid = isValid || !isInputTouched; const inputChangeHandler = useCallback(event => { setValue(event.target.value); if(!isInputTouched) { setIsInputTouched(true); } }, [isInputTouched]); const inputBlurHandler = useCallback(() => { setIsInputTouched(true); }, []); const reset = useCallback(() => { setValue(''); setIsInputTouched(false); }, []); return { value, isValid, isInputValid, message, inputChangeHandler, inputBlurHandler, reset }; } export default useInput; </pre> <p>我目前在 Input.js 中使用這個自訂鉤子,如下所示:</p> <p><em>components/UI/Input.js</em></p> <pre class="brush:js;toolbar:false;">import { useEffect } from 'react'; import useInput from '../../hooks/use-input'; import './Input.scss'; function Input(props) { const { value, isValid, isInputValid, message, inputChangeHandler, inputBlurHandler, reset } = useInput(props.validity); const { onIsInputValidOrMessageChange, onValidityChange, onReset } = props; let className = 'form-control'; if(!isInputValid) { className = `${className} form-control--invalid`; } if(props.className) { className = `${className} ${props.className}`; } useEffect(() => { if(onIsInputValidOrMessageChange && typeof onIsInputValidOrMessageChange === 'function') { onIsInputValidOrMessageChange(isInputValid, message); } }, [onIsInputValidOrMessageChange, isInputValid, message]); useEffect(() => { if(onValidityChange && typeof onValidityChange === 'function') { onValidityChange(isValid); } }, [onValidityChange, isValid]); useEffect(() => { if(onReset && typeof onReset === 'function') { onReset(reset); } }, [onReset, reset]); return ( <input {...props} className={className} value={value}onChange={inputChangeHandler} onBlur={inputBlurHandler} /> ); } export default Input; </pre> <p>在輸入元件中,我直接使用 <code>isInputValid</code> 狀態將無效的 CSS 類別加入輸入。但我也會<code>isInputValid</code>、<code>message</code>、<code>isValid</code> 狀態和<code>reset</code>/code> 狀態和<code>reset</code>在其中使用。為了傳遞這些狀態和函數,我使用在props 中定義的<code>onIsInputValidOrMessageChange</code>、<code>onValidityChange</code>、<code>onReset</code>/code>、<code>onReset</code>相反,從孩子到父母)。 </p> <p>這是 FormGroup 元件的定義以及我如何使用 FormGroup 內的輸入狀態來顯示驗證訊息(如果有):</p> <p><em>components/UI/FormGroup.js</em></p> <pre class="brush:js;toolbar:false;">import { useState } 從 'react'; import Input from './Input'; import './FormGroup.scss'; function FormGroup(props) { const [message, setMessage] = useState(null); const [isInputValid, setIsInputValid] = useState(false); let className = 'form-group'; if(props.className) { className = `form-group ${props.className}`; } let labelCmp = ( <label htmlFor={props.id}> {props.label} </label> ); if(props.sideLabelElement) { labelCmp = ( <div className="form-label-group"> {labelCmp} {props.sideLabelElement} </div> ); } function isInputValidOrMessageChangeHandler(changedIsInputValid, changedMessage) { setIsInputValid(changedIsInputValid); setMessage(changedMessage); } return ( <div className={className}> {labelCmp} <Input id={props.id} onIsInputValidOrMessageChange={isInputValidOrMessageChangeHandler} {...props.inputProps} /> {!isInputValid && <p>{message}</p>} </div> ); } export default FormGroup; </pre> <p>從上面的程式碼可以看到,我定義了<code>message</code> 和<code>isInputValid</code> 狀態來儲存更新的<code>message</code> 和; 狀態來儲存更新的<code>message</code> 和; <code>isInputValid</code> code> 從輸入元件傳遞的狀態。我已經在輸入元件中定義了 2 個狀態來保存這些值,但我需要在此元件中定義另外 2 個狀態來儲存輸入元件中更新和傳遞的值。這有點奇怪,對我來說似乎不是最好的方式。 </p> <p><strong>問題是:</strong>我想我可以使用 React Context (useContext) 或 React Redux 來解決這裡的 prop 鑽取問題。但我不確定我目前的狀態管理是否不好,是否可以使用 React Context 或 React Redux 來改善。因為根據我的了解,React Context 在狀態頻繁變化的情況下可能會很糟糕,但如果 Context 在應用程式範圍內使用,那麼這是有效的。在這裡,我可以建立一個上下文來儲存和更新整個表單,從而實現表單範圍內的擴充。另一方面,React Redux 可能不是最適合的孤島,並且可能有點矯枉過正。你們有什麼感想?對於這種特定情況,什麼可能是更好的替代方案? </p> <p><strong>注意:</strong>由於我是 React 的新手,因此我願意接受您關於我所有編碼的所有建議,從簡單的錯誤到一般的錯誤。謝謝! </p>
P粉419164700P粉419164700436 天前554

全部回覆(2)我來回復

  • P粉627136450

    P粉6271364502023-09-02 15:06:02

    關於 React 狀態管理有兩種主要學派:受控和非受控。受控表單可能會使用 React 上下文進行控制,其中可以在任何地方存取值以提供反應性。但是,受控輸入可能會導致效能問題,尤其是在每個輸入上更新整個表單時。這就是不受控制的表單出現的地方。透過這個範例,所有狀態管理都必須利用瀏覽器的本機功能來顯示狀態。這種方法的主要問題是你失去了表單的 React 方面,你需要在提交時手動收集表單數據,並且為此維護多個引用可能很乏味。

    受控輸入如下圖所示:

    const [name, setName] = useState("");
    
    return <input value={name} onChange={(e) => setName(e.currentTarget.value)} />

    編輯:正如@Arkellys指出的那樣,您不一定需要引用來收集表單數據,這是一個使用 FormData

    的範例

    並且不受控制:

    const name = useRef(null);
    const onSubmit = () => {
        const nameContent = name.current.value;
    }
    return <input ref={name} defaultValue="" />

    從這兩個範例中可以明顯看出,使用任一方法維護多元件表單都是乏味的,因此,通常使用函式庫來幫助您管理表單。我個人推薦 React Hook Form 作為經過實戰測試、維護良好且易於使用的表單圖書館。它採用不受控制的形式來實現最佳效能,同時仍允許您觀看單一輸入以進行反應式渲染。

    關於是否使用 Redux、React 上下文或任何其他狀態管理系統,假設您正確實現的話,通常在效能方面沒有什麼區別。如果您喜歡 flux 架構,那麼請務必使用 Redux,但在大多數情況下,React 上下文是既高效又足夠。

    您的useInput 自訂掛鉤看起來是解決問題react-hook-formreact-final-form 的勇敢但誤導性的嘗試代碼>已經解決了。您正在創建不必要的複雜性和不可預測的副作用 有了這個抽象。此外,您鏡像 props 這通常是 React 中的反模式。

    如果您確實想實現自己的表單邏輯(我建議您不要這樣做,除非是出於教育目的),您可以遵循以下準則:

    1. 在最高共同祖先處保留一個事實來源
    2. 避免鏡像和複製狀態
    3. 使用 useMemouseRef 盡可能少地重新渲染

    回覆
    0
  • P粉596191963

    P粉5961919632023-09-02 11:36:55

    這是我用來在 Redux 等發布-訂閱庫和透過元件樹傳播狀態之間做出決定的一個直接面向。

    如果兩個元件具有父子關係並且彼此距離最多兩邊,則將子狀態傳播到父級

    父級 -> child1-level1 -> child1-level2 ------ 好

    父級 -> child1-level1 ------ 好

    父級 -> child1-level1 -> child1-level2 -> child1-level3 --> 行程過多,無法將狀態從 child1-level3 變更為父級

    • 如果互動元件之間的邊距離超過 2 個,則使用 redux
    • 對同級元件使用 redux,即共享父元件且需要彼此通訊的子元件(在側邊面板中選擇一個樹項目,在主元件中顯示所選項目的詳細資訊)

    自實作以來

    • 我發現 useInput 是一種過度重構,您的輸入元件應該足以管理與輸入相關的操作,更好地抽象驗證等方面
    • 您可以在表單提交時觸發所有驗證,在這種情況下,您不需要受控輸入(在表單的 onSubmit 事件上附加驗證)
    • 但是,如果您的表單包含太多字段(例如>5),並且您想在提交之前驗證該字段,您可以使用輸入字段的onBlur 事件,或者使用onInput 以及debounce 操作(例如來自lodash 的debounce操作)或像這樣實作

    function debounce(func, timeout = 300){
      let timer;
      return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => { func.apply(this, args); }, timeout);
      };
    }

    回覆
    0
  • 取消回覆