react表单受控的实现方案

背景

数据的受控控制一直是react里的一个痛点,当我想要实现一个输入框的受控控制时,我需要定义一个onChangevalue ,手动去实现数据的绑定。当受控的元素一多,便会出现满屏的set

笔者所在的公司业务比较大,偏向于后台管理的sass系统,用户群体比较大,其中就包括有谷歌这些用户。自然迭代更新速度也比较快,而随着不断的迭代更新,项目也是日益庞大。在日常需求中,表单的开发就占据了大部分场景,而在用react开发表单这块,特别是当表单字段过于复杂,表单过于庞大时,开发受控表单也要投入不小的开发生产力和不少的受控代码,不说优雅和后期的维护,对于页面响应速度来说,也是会随着字段的增加而变的越来越慢,即使拆分成颗粒度最小的组件。

在一个表单业务中,字段A依赖于字段B,字段C又依赖于字段A的变化,而字段C追踪依赖后又要实时渲染在视图里。这是很常见的需求场景,当组织这些依赖的时候,随之而来的考虑的是一个性能问题,我们很常见的一个做法便是状态的提升,将它们都放到顶层容器中,统一管理。但是这样会随着依赖的不断增加,而造成当前渲染的树不断渲染,当越来越多的字段沉积,不断的重新渲染,直到最后页面奔溃,内存溢出。

当这种做法产生了较大的副作用后,我们这时候会开始考虑改变做法,优化代码,考虑在各自的组件中定义onchangevalue维护这个字段,然后在注入到全局状态管理中,在需要用到的组件中订阅这个字段,这样就减少了重复渲染的次数。

在这个过程中,我们通常需要定义一系列受控代码,以达到我们的预期。

第三方库

当然,如果在使用第三方UI库的时候,通常会提供Form组件,比如antd 。而 antd的Form强依赖于它本身的表单控件,而且对于定义一个表单而言,所定义的受控逻辑略微繁琐。表单业务复杂时,需要传入一系列的prop和方法。当需要与别的UI结合的时候,Form组件就失去了他的意义。

笔者之前所用的是react-hook-formreact-hook-form 能够轻易集成第三方UI,同样的,对于数据受控以及数据订阅方面,rect-hook-form 需要定义的代码同样不少,比如使一个表单项受控,需要显示引入它的Controller组件包裹。比如需要一个状态去实时反应到表单之外的视图的时候,要另外定义一个变量结合它的react-hook-form的watch去维护这个变量的状态。

而其实在开发过程中,我们并不想要关心这种受控过程,只想要知道受控的结果和应用受控状态。当定义好一套表单模型数据时,并将这套模型与他对应的表单项视图关联起来的时候,它们之间应该自动建立起一个受控的桥梁和纽带,开发者不需要知道这个桥梁是怎么建立的,不需要去维护这个桥梁,开发者需要做的,就是去应用这套模型。而这套模型,永远是最新的。并且不影响到它们外部的其他元素。

React-form-simple

react-form-simple是一个基于react的可受控可扩展的轻量级表单库,以最快的速度以及最精简的代码渲染出一个可受控的表单。React-form-simple除了集成自身功能之外,还具有非常可扩展的接口, 并可与第三方ui集成使用。

react-hook-form 启发, 得源于 react-hook-form 的设计灵感,笔者所在团队花费了两个月时间梳理了项目的全部表单,综合整理并开发出了一款轻量级表单库。该库已重写了项目的大部分表单,并已在生产环境中使用了大半年之久。

react-form-simple 基于 es6Proxy 创建一个可观察的表单模型对象,该库有如下特点:

  • 通过创建一个可观察对象来观察表单的模型操作, 表单项的受控直接通过_. 赋值。

  • 简单几行代码就可以完成表单受控, 无需关心受控逻辑, 无需关心受控过程, 只需要知道受控结果和如何应用你的受控状态。

  • 每个表单项之间的渲染自动完全隔离, 不需要自行组织组件隔离。这将能够更快的处理表单输入后的响应速度, 以及很大程度的避免在大型动态数据下造成的页面卡顿。

  • 具有数据观测功能, 可以在某些场景下对整个表单或者某个具体的表单项进行单一或者统一的观察监测, 可以在你需要用表单项最新的值进行渲染的地方进行值的订阅。

  • 灵活的使用方式, 灵活的页面布局组合, 开发者可以根据自己的喜好和场景使用某种方式以及内置布局。在大多数场景下, 无需开发者手动布局。

  • 简约的 API 设计, 在操作表单的过程中, 简单的只需要引入两个 API, 就可以完成大部分工作。

  • 高度可扩展的表单接口, 在一些复杂需求或者定制化场景中, 开发者可以自行定制表单的控制逻辑。

  • 可以轻易集成在你的 UI 或者 第三方库中。

  • 完整的类型推断。

单元测试覆盖

使用

jsx 复制代码
npm install react-form-simple -S

简化表单受控

react-form-simple 暴露出一个 render 方法,在一般情况下, 传入表单模型字段和渲染视图,便能搭建它们之间的受控桥梁。

js 复制代码
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>
    </>
  );
}

如上例子,创建一个受控表单只需要两行代码。

  • 通过useForm创建一个表单数据模型。
  • 使用useForm暴露出的render方法创建表单项与渲染视图的受控桥梁。

开发者不需要知道name字段与input的受控过程,不需要关心他们是如何受控的,开发者需要做的,就是将model 模型数据如何运用在代码中。更多用法请查看文档

而在name字段与 input 视图受控时,它们之间的变化不会重新导致外部的任何重复渲染。也就是说,表单的渲染都是完全相互隔离的。

订阅最新值

在一个表单开发中,通常有A字段依赖于B字段的场景,比如在B字段的值发生改变后,A字段需要做相应的逻辑处理,并将A字段的值实时渲染在视图里。

react-form-simple 里,表单项的受控并不能直接引起外部视图的刷新,可以借助 useSubscribe 来订阅某个字段或者整个表单,以将它渲染在表单视图之外。

js 复制代码
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 发生变化的时候,subscribeName 便会实时打印。

但是一般不推荐在父级组件中来订阅, 因为这会引起整个渲染树的更新, 推荐的做法是只用在需要订阅的地方, 可以通过 props 透传, 也可以将它注入到全局状态管理中。

下面这个例子展示的是有两个输入框,当其中一个输入框的值等于 amount 的时候,便显示另外一个输入框。

js 复制代码
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监听

react-form-simple 提供数据观测功能,使用 useWatch 可以观察某个字段或者整个表单的变化。

js 复制代码
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 方法可以快速的对表单模型进行校验。

js 复制代码
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 的例子。

js 复制代码
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>
    </>
  );
}

无论开发者使用的什么UI库,react-form-simple 都可以很好的与它们集成在一起。

组件形式

在开发者使用的习惯上,可能有些开发人员习惯于以组件的形式来渲染视图,这可以更加直观的组织代码。 react-form-simple 暴露出了两个组件 FormFormItem 来提供给开发人员使用组件形式来创建表单。在需要定制化表单,或者处理一些额外的逻辑的时候,这两个组件将非常有用。

开发者可以基于此来封装适合自己使用习惯的 useForm,以此来定制化的开发人员的表单hook。

使用 FormItem 例子,关于更多介绍请查看FormItem

js 复制代码
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>
    </>
  );
}

定制化表单

开发者可以传入一个普通的表单对象完全自定义表单的受控逻辑来定制化表单,而无需依赖于 useForm hook。

js 复制代码
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>
  );
}

关于更多定制化表单,请查看定制化表单

文档地址

结语

本文简单的探讨了react表单的受控实现方案以及react-form-simple的使用,构建可维护的代码。以及在新技术领域保持学习的动力。通过这些话题,我们不仅仅是在谈论代码本身,更是在谈论一种持续的挑战与成长的过程。

在编码的旅程中,我们常常会面临新的问题,需要找到创新的解决方案。正是通过解决这些挑战,我们才能不断提升自己的技能,并在技术的海洋中航行得更远。每一行代码都是一次思考的结果,每一个问题都是一次成长的机会。

在未来的代码之旅中,愿我们能够保持对技术的热爱,持续学习,不断挑战自己。编码不仅仅是一项技能,更是一场不断演化的冒险。感谢你阅读本文,期待与你在下一篇文章中再次相遇。

Happy coding! 🚀✨

相关推荐
前端小小王1 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发1 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
不是鱼6 小时前
构建React基础及理解与Vue的区别
前端·vue.js·react.js
飞翔的渴望9 小时前
antd3升级antd5总结
前端·react.js·ant design
╰つ゛木槿12 小时前
深入了解 React:从入门到高级应用
前端·react.js·前端框架
用户305875848912515 小时前
Connected-react-router核心思路实现
react.js
哑巴语天雨1 天前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情1 天前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js
码农老起1 天前
掌握 React:组件化开发与性能优化的实战指南
react.js·前端框架
前端没钱1 天前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js