react表單受控的實現(xiàn)方案
背景
數(shù)據(jù)的受控控制一直是react里的一個痛點,當我想要實現(xiàn)一個輸入框的受控控制時,我需要定義一個onChange和value,手動去實現(xiàn)數(shù)據(jù)的綁定。當受控的元素一多,便會出現(xiàn)滿屏的set。
筆者所在的公司業(yè)務(wù)比較大,偏向于后臺管理的sass系統(tǒng),用戶群體比較大,其中就包括有谷歌這些用戶。自然迭代更新速度也比較快,而隨著不斷的迭代更新,項目也是日益龐大。在日常需求中,表單的開發(fā)就占據(jù)了大部分場景,而在用react開發(fā)表單這塊,特別是當表單字段過于復(fù)雜,表單過于龐大時,開發(fā)受控表單也要投入不小的開發(fā)生產(chǎn)力和不少的受控代碼,不說優(yōu)雅和后期的維護,對于頁面響應(yīng)速度來說,也是會隨著字段的增加而變的越來越慢,即使拆分成顆粒度最小的組件。
在一個表單業(yè)務(wù)中,字段A依賴于字段B,字段C又依賴于字段A的變化,而字段C追蹤依賴后又要實時渲染在視圖里。這是很常見的需求場景,當組織這些依賴的時候,隨之而來的考慮的是一個性能問題,我們很常見的一個做法便是狀態(tài)的提升,將它們都放到頂層容器中,統(tǒng)一管理。但是這樣會隨著依賴的不斷增加,而造成當前渲染的樹不斷渲染,當越來越多的字段沉積,不斷的重新渲染,直到最后頁面奔潰,內(nèi)存溢出。
當這種做法產(chǎn)生了較大的副作用后,我們這時候會開始考慮改變做法,優(yōu)化代碼,考慮在各自的組件中定義onchange和value維護這個字段,然后在注入到全局狀態(tài)管理中,在需要用到的組件中訂閱這個字段,這樣就減少了重復(fù)渲染的次數(shù)。
在這個過程中,我們通常需要定義一系列受控代碼,以達到我們的預(yù)期。
第三方庫
當然,如果在使用第三方UI庫的時候,通常會提供Form組件,比如antd。而 antd的Form強依賴于它本身的表單控件,而且對于定義一個表單而言,所定義的受控邏輯略微繁瑣。表單業(yè)務(wù)復(fù)雜時,需要傳入一系列的prop和方法。當需要與別的UI結(jié)合的時候,F(xiàn)orm組件就失去了他的意義。

筆者之前所用的是react-hook-form,react-hook-form能夠輕易集成第三方UI,同樣的,對于數(shù)據(jù)受控以及數(shù)據(jù)訂閱方面,rect-hook-form需要定義的代碼同樣不少,比如使一個表單項受控,需要顯示引入它的Controller組件包裹。比如需要一個狀態(tài)去實時反應(yīng)到表單之外的視圖的時候,要另外定義一個變量結(jié)合它的react-hook-form的watch去維護這個變量的狀態(tài)。

而其實在開發(fā)過程中,我們并不想要關(guān)心這種受控過程,只想要知道受控的結(jié)果和應(yīng)用受控狀態(tài)。當定義好一套表單模型數(shù)據(jù)時,并將這套模型與他對應(yīng)的表單項視圖關(guān)聯(lián)起來的時候,它們之間應(yīng)該自動建立起一個受控的橋梁和紐帶,開發(fā)者不需要知道這個橋梁是怎么建立的,不需要去維護這個橋梁,開發(fā)者需要做的,就是去應(yīng)用這套模型。而這套模型,永遠是最新的。并且不影響到它們外部的其他元素。
React-form-simple

react-form-simple是一個基于react的可受控可擴展的輕量級表單庫,以最快的速度以及最精簡的代碼渲染出一個可受控的表單。React-form-simple除了集成自身功能之外,還具有非??蓴U展的接口, 并可與第三方ui集成使用。
受 react-hook-form 啟發(fā), 得源于 react-hook-form 的設(shè)計靈感,筆者所在團隊花費了兩個月時間梳理了項目的全部表單,綜合整理并開發(fā)出了一款輕量級表單庫。該庫已重寫了項目的大部分表單,并已在生產(chǎn)環(huán)境中使用了大半年之久。
react-form-simple 基于 es6 的 Proxy 創(chuàng)建一個可觀察的表單模型對象,該庫有如下特點:
通過創(chuàng)建一個可觀察對象來觀察表單的模型操作, 表單項的受控直接通過
_.賦值。簡單幾行代碼就可以完成表單受控, 無需關(guān)心受控邏輯, 無需關(guān)心受控過程, 只需要知道受控結(jié)果和如何應(yīng)用你的受控狀態(tài)。
每個表單項之間的渲染自動完全隔離, 不需要自行組織組件隔離。這將能夠更快的處理表單輸入后的響應(yīng)速度, 以及很大程度的避免在大型動態(tài)數(shù)據(jù)下造成的頁面卡頓。
具有數(shù)據(jù)觀測功能, 可以在某些場景下對整個表單或者某個具體的表單項進行單一或者統(tǒng)一的觀察監(jiān)測, 可以在你需要用表單項最新的值進行渲染的地方進行值的訂閱。
靈活的使用方式, 靈活的頁面布局組合, 開發(fā)者可以根據(jù)自己的喜好和場景使用某種方式以及內(nèi)置布局。在大多數(shù)場景下, 無需開發(fā)者手動布局。
簡約的 API 設(shè)計, 在操作表單的過程中, 簡單的只需要引入兩個 API, 就可以完成大部分工作。
高度可擴展的表單接口, 在一些復(fù)雜需求或者定制化場景中, 開發(fā)者可以自行定制表單的控制邏輯。
可以輕易集成在你的 UI 或者 第三方庫中。
完整的類型推斷。
單元測試覆蓋

使用
npm install react-form-simple -S
簡化表單受控
react-form-simple 暴露出一個 render 方法,在一般情況下, 傳入表單模型字段和渲染視圖,便能搭建它們之間的受控橋梁。
import React, { useEffect } from 'react';
import { useForm } from 'react-form-simple';
export default function App() {
const { render, model } = useForm({ name: '' });
const renderName = render('name')(<input />);
const onSubmit = () => void console.log(model);
return (
<>
{renderName}
<button onClick={onsubmit}>submit</button>
</>
);
}
如上例子,創(chuàng)建一個受控表單只需要兩行代碼。
- 通過useForm創(chuàng)建一個表單數(shù)據(jù)模型。
- 使用useForm暴露出的render方法創(chuàng)建表單項與渲染視圖的受控橋梁。
開發(fā)者不需要知道name字段與input的受控過程,不需要關(guān)心他們是如何受控的,開發(fā)者需要做的,就是將model 模型數(shù)據(jù)如何運用在代碼中。更多用法請查看文檔
而在name字段與 input 視圖受控時,它們之間的變化不會重新導(dǎo)致外部的任何重復(fù)渲染。也就是說,表單的渲染都是完全相互隔離的。
訂閱最新值
在一個表單開發(fā)中,通常有A字段依賴于B字段的場景,比如在B字段的值發(fā)生改變后,A字段需要做相應(yīng)的邏輯處理,并將A字段的值實時渲染在視圖里。
在 react-form-simple 里,表單項的受控并不能直接引起外部視圖的刷新,可以借助 useSubscribe 來訂閱某個字段或者整個表單,以將它渲染在表單視圖之外。
import React from 'react';
import { useForm } from 'react-form-simple';
export default function App() {
const { render, useSubscribe } = useForm({ name: 'name' });
const renderName = render('name')(<input />);
const subscribeName = useSubscribe(({ model }) => model.name);
console.log({ subscribeName });
return (
<>
{renderName}
</>
);
}
如上所示,在 name 發(fā)生變化的時候,subscribeName 便會實時打印。
但是一般不推薦在父級組件中來訂閱, 因為這會引起整個渲染樹的更新, 推薦的做法是只用在需要訂閱的地方, 可以通過 props 透傳, 也可以將它注入到全局狀態(tài)管理中。
下面這個例子展示的是有兩個輸入框,當其中一個輸入框的值等于 amount 的時候,便顯示另外一個輸入框。
import { useForm } from "react-form-simple";
export default function App() {
const { render, useSubscribe } = useForm({ name: "", amount: "" });
const renderName = render("name")(<input />);
const nameValue = useSubscribe(({ model }) => model.name);
const renderAmount = nameValue === "amount" && render("amount")(<input />);
return (
<>
{renderAmount}
{renderName}
</>
);
}
watch監(jiān)聽
react-form-simple 提供數(shù)據(jù)觀測功能,使用 useWatch 可以觀察某個字段或者整個表單的變化。
import { useForm } from 'react-form-simple';
export default function App() {
const { render, useWatch } = useForm({ name: 'name', age: 'age' });
const renderName = render('name')(<input className="input" />);
const renderAge = render('age')(<input className="input" />);
useWatch(
({ model }) => [model?.name, model?.age],
(value, preValue) => {
console.log({ value, preValue });
},
);
return (
<>
{renderName}
{renderAge}
</>
);
}
表單校驗
通過 useForm 暴露出的 validate 方法可以快速的對表單模型進行校驗。
import Button from '@components/Button';
import React from 'react';
import { useForm } from 'react-form-simple';
export default function App() {
const { render, validate, model, clearValidate, setError } = useForm({
name: '',
age: '',
});
const renderName = render('name', {
rules: { required: 'Please Input' },
requireIndicator: true,
label: 'name',
})(<input className="input" />);
const renderAge = render('age', {
label: 'age',
rules: [
{ required: 'Please Input' },
{
validator(value) {
if (value < 10) {
return 'Min 10';
}
return '';
},
},
],
})(<input className="input" />);
const renderSubmit = (
<Button
onClick={async () => {
await validate();
console.log(model);
}}
>
Submit
</Button>
);
const renderclear = (
<Button
onClick={async () => {
clearValidate();
}}
>
clear
</Button>
);
return (
<>
{renderName}
{renderAge}
{renderSubmit}
{renderclear}
</>
);
}
集成第三方UI
在實際項目中,我們通常需要用到第三方UI來渲染視圖,rect-form-simple 可以很輕易的與這些UI庫集成在一起,無論什么UI庫。
下面的例子是集成 antd 的例子。
import Button from '@components/Button';
import { Checkbox, Input, Select } from 'antd';
import React from 'react';
import { useForm } from 'react-form-simple';
export default function App() {
const { render, model, validate } = useForm(
{ name: '', select: 'jack', checkbox: true },
{ labelPosition: 'top' },
);
const renderName = render('name', {
label: 'name',
rules: { required: 'please Input' },
requireIndicator: true,
defineProps(options) {
return { status: options.isError ? 'error' : '' };
},
})(<Input style={{ width: '300px' }} placeholder="Please Input" />);
const renderSelect = render('select', {
label: 'age',
formatChangeValue: (e) => e,
})(
<Select
options={[
{ value: 'jack', label: 'Jack' },
{ value: 'lucy', label: 'Lucy' },
{ value: 'Yiminghe', label: 'yiminghe' },
]}
style={{ width: '300px' }}
/>,
);
const renderCheckbox = render('checkbox', {
label: 'Checkbox',
labelPosition: 'row',
})(<Checkbox />);
const renderSubmit = (
<Button
onClick={async () => {
await validate();
console.log(model);
}}
>
Submit
</Button>
);
return (
<>
{renderName}
{renderSelect}
{renderCheckbox}
<div>{renderSubmit}</div>
</>
);
}
無論開發(fā)者使用的什么UI庫,react-form-simple 都可以很好的與它們集成在一起。
組件形式
在開發(fā)者使用的習(xí)慣上,可能有些開發(fā)人員習(xí)慣于以組件的形式來渲染視圖,這可以更加直觀的組織代碼。 react-form-simple 暴露出了兩個組件 Form 和 FormItem 來提供給開發(fā)人員使用組件形式來創(chuàng)建表單。在需要定制化表單,或者處理一些額外的邏輯的時候,這兩個組件將非常有用。
開發(fā)者可以基于此來封裝適合自己使用習(xí)慣的 useForm,以此來定制化的開發(fā)人員的表單hook。
使用 FormItem 例子,關(guān)于更多介紹請查看FormItem
import Button from '@components/Button';
import React from 'react';
import { FormItem, useForm } from 'react-form-simple';
export default function App() {
const { contextProps, model, validate } = useForm({
name: '',
age: 'age',
});
return (
<>
<FormItem
defaultValue={model.name}
rules={{ required: 'Please Input' }}
bindId="name"
getContent={({ attrs }) => <input {...attrs} className="input" />}
contextProps={contextProps}
/>
<FormItem
defaultValue={model.age}
rules={{ required: 'Please Select' }}
bindId="age"
getContent={({ attrs }) => {
return (
<select {...attrs}>
<option value="name">name</option>
<option value="age">age</option>
<option value="email">email</option>
</select>
);
}}
contextProps={contextProps}
/>
<Button
onClick={async () => {
await validate();
console.log(model);
}}
>
Submit
</Button>
</>
);
}
定制化表單
開發(fā)者可以傳入一個普通的表單對象完全自定義表單的受控邏輯來定制化表單,而無需依賴于 useForm hook。
import Button from '@components/Button';
import React, { useEffect, useRef } from 'react';
import {
Form,
FormItem,
type ContextProps,
type FormApis,
} from 'react-form-simple';
export default function App() {
const formRef = useRef<FormApis>(null);
const model = useRef({
name: '',
}) as any;
const contextProps = useRef<ContextProps>({
updated({ bindId, value }) {
model.current[bindId] = value;
},
reset({ bindId }) {
model.current[bindId] = '';
formRef.current?.setValue(bindId, '');
},
});
useEffect(() => {
const values = { name: 'name' };
model.current = values;
formRef.current?.setValues(values);
}, []);
const renderName = (
<FormItem
bindId="name"
rules={{ required: 'Please Input' }}
label="name"
getContent={({ attrs }) => {
return (
<input placeholder="Please Input" {...attrs} className="input" />
);
}}
/>
);
return (
<Form
ref={formRef}
contextProps={contextProps.current}
direction="column"
labelWidth="40px"
>
{renderName}
<FormItem label=" ">
<Button
onClick={() => {
console.log(model.current);
}}
>
submit
</Button>
<Button
style={{ marginLeft: '15px' }}
plain
onClick={() => {
formRef.current?.reset();
}}
>
reset
</Button>
</FormItem>
</Form>
);
}
關(guān)于更多定制化表單,請查看定制化表單
結(jié)語
本文簡單的探討了react表單的受控實現(xiàn)方案以及react-form-simple的使用,構(gòu)建可維護的代碼。以及在新技術(shù)領(lǐng)域保持學(xué)習(xí)的動力。通過這些話題,我們不僅僅是在談?wù)摯a本身,更是在談?wù)撘环N持續(xù)的挑戰(zhàn)與成長的過程。
在編碼的旅程中,我們常常會面臨新的問題,需要找到創(chuàng)新的解決方案。正是通過解決這些挑戰(zhàn),我們才能不斷提升自己的技能,并在技術(shù)的海洋中航行得更遠。每一行代碼都是一次思考的結(jié)果,每一個問題都是一次成長的機會。
在未來的代碼之旅中,愿我們能夠保持對技術(shù)的熱愛,持續(xù)學(xué)習(xí),不斷挑戰(zhàn)自己。編碼不僅僅是一項技能,更是一場不斷演化的冒險。感謝你閱讀本文,期待與你在下一篇文章中再次相遇。
以上就是react表單受控的實現(xiàn)方案的詳細內(nèi)容,更多關(guān)于react表單受控的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解React Native 采用Fetch方式發(fā)送跨域POST請求
這篇文章主要介紹了詳解React Native 采用Fetch方式發(fā)送跨域POST請求,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11
關(guān)于?React?中?useEffect?使用問題淺談
本文主要介紹了關(guān)于React中useEffect使用問題淺談,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
React中常見的TypeScript定義實戰(zhàn)教程
這篇文章主要介紹了React中常見的TypeScript定義實戰(zhàn),本文介紹了Fiber結(jié)構(gòu),F(xiàn)iber的生成過程,調(diào)和過程,以及 render 和 commit 兩大階段,需要的朋友可以參考下2022-10-10
React函數(shù)式組件Hook中的useEffect函數(shù)的詳細解析
useEffect是react v16.8新引入的特性。我們可以把useEffect hook看作是componentDidMount、componentDidUpdate、componentWillUnmounrt三個函數(shù)的組合2022-10-10
詳解React??App.js?文件的結(jié)構(gòu)和作用
在React應(yīng)用中,App.js文件通常是項目的根組件文件,它負責(zé)組織和渲染其他組件,是應(yīng)用的核心部分,本文將詳細介紹App.js文件的結(jié)構(gòu)、作用和最佳實踐,感興趣的朋友跟隨小編一起看看吧2024-08-08

