React 写表单不用别的,用这个状态库就够了大家好,我是 OQ(Open Quoll),今天想要聊一下 表单。表单 是
大家好,我是 OQ(Open Quoll),今天想要聊一下 表单。
表单 是大部分 React 应用中状态最丰富的部分,尽管有一些专门处理表单的库,但是表单库 API 都是以特定的表单场景设计的,一旦实际情况超出预想,用法就会变得诡异起来,很难继续招架问题,所以 单纯的表单库实际上是解决不好表单问题的。
我一直认为不断 以最少的工具快速解决最多的问题 才是一个开发者得以长青的根源,因为这意味着以最高的投入产出比 有效输出 和 持续积累。所以我写了 React Mug,一个简洁的函数式状态库,让所有的状态问题回归到用一个工具来快速解决。
而表单问题归根结底还是状态问题,解决好了状态也就解决好了表单,所以 好用通用的状态库才是解决表单问题的终极解。下面我来用 React Mug 实现一个表单案例一起来看一下用状态库是如何解决表单问题的。
(友情提示:本文中部分 API 仅适用于 react-mug@0.3.x
及以下版本。)
表单案例功能
为了简洁有效,这里用一个简化的 “社区活动报名” 表单作为案例,其中的字段有:
- 输入 “姓名” 的文本框,默认为空
- 控制是否 “填写身高” 的复选框,默认未选中
- 输入 “身高” 的文本框。默认为 170,只在 “填写身高” 选中时可见
在输入字段时实时校验并提示校验错误。
在提交表单时校验全部的可见字段,在存在校验错误时提示修改,在不存在错误时提交数据。
在提交数据的过程中禁用提交按钮。
在提交数据过后提示成功失败信息并在 3 秒后清空信息,只在提交成功时重置表单。
下面开始着手实现。
状态类型 和 接口调用
首先定义一下 表示字段的通用类型 Field<T>
:
interface Field<T> {
value: T; // 字段的值
error?: string; // 校验错误
}
表单整体的状态类型 EnrollState
:
interface EnrollState {
name: Field<string>; // “姓名” 字段
includingHeight: Field<boolean>; // “填写身高” 字段
height: Field<number>; // “身高” 字段
submit: {
loading: boolean; // 正在提交数据
hint?: string; // 存在校验错误时的修改提示
success?: string; // 提交成功提示
failure?: string; // 提交失败提示
};
}
和 提交数据的接口调用 postEnrollment
async function postEnrollment(params: { name: string; height?: number }): Promise<void> {
// 调用接口提交数据
}
状态容器
然后定义一下盛装表单状态的容器。
在 React Mug 中 Mug(马克杯)便是盛装状态的容器,它可以由任何一个顶层字段含有 [construction]
的对象表示,construction
是库提供的全局唯一的 Symbol,[construction]
的字段值即是这个 Mug 所盛装状态的初始值,Mug 的类型可以通过 Mug
帮助类型辅助定义。
盛装表单状态的 Mug enrollMug
定义如下:
import { construction, Mug } from 'react-mug';
const enrollMug: Mug<EnrollState> = {
[construction]: {
name: { value: '' }, // “姓名” 默认为空
includingHeight: { value: false }, // “填写身高” 默认未选中
height: { value: 170 }, // “身高” 默认为 170
submit: { loading: false }, // 默认没有正在提交数据
},
};
状态操作
接下来是定义如何操作这个状态。
在 React Mug 中 对 Mug 所盛装状态的读写操作分别是通过调用 读操作器 和 写操作器 完成的,库预制提供了一个直接返回 Mug 状态的读操作器 check
和一个按 合并逻辑 修改 Mug 状态的写操作器 swirl
,用法分别如下:
import { check } from 'react-mug';
// 示例
const enrollState = check(enrollMug);
import { swirl } from 'react-mug';
// 示例
swirl(enrollMug, { submit: { loading: false } });
并且它们可以接收状态当纯函数调用:
// 示例
const checkedEnrollState = check(enrollState);
// 示例
const swirledEnrollState = swirl(enrollState, { submit: { loading: false } });
另外写操作器的返回值是当前调用的入参 Mug,所以可以这样嵌套调用:
// 示例
swirl(swirl(enrollMug, { submit: { hint: '修改提示' } }), { submit: { loading: false } });
预制的操作器非常适用于简单琐碎的状态操作。
但是对于复杂点的状态操作,自定义操作器会更适合,通过帮助函数 r
和 w
可以分别自定义读写操作器。
自定义写操作器
在 React Mug 中可以 向帮助函数 w
中传入一个以目标状态为第一个参数并返回目标状态的纯函数来自定义一个写操作器。
功能中提到 “在输入字段时实时校验并提示校验错误”,所以这里为每个字段自定义 “更新、校验、提示错误” 的写操作器。
首先为 “姓名” 字段定义写操作器 setName
:
import { r } from 'react-mug';
const setName = w((state: EnrollState, value: string) => ({
...state,
name: { value, error: validateName(value) },
}));
function validateName(name: string) {
if (!name) {
return '姓名不可以为空';
}
if (name.length > 10) {
return '姓名长度不能超过 10 个字符';
}
}
它的用法与 swirl
类似:
// 示例
setName(enrollMug, '张三');
并且它也可以接收状态当纯函数调用:
// 示例
const changedEnrollState = setName(enrollState, '张三');
然后类似地,为 “身高” 字段定义写操作器 setHeight
:
const setHeight = w((state: EnrollState, value: string) => {
const newValue = parseInt(value) || 0;
return {
...state,
height: { value: newValue, error: validateHeight(newValue) },
};
});
function validateHeight(height: number) {
if (height < 50) {
return '身高不能小于 50 厘米';
}
if (height > 300) {
return '身高不能大于 300 厘米';
}
}
而至于 “填写身高” 字段,它的写操作不涉及校验比较简单,所以后面像这样直接调用 swirl
就可以了:
// 示例
swirl(enrollMug, { includingHeight: { value: true } });
此外,功能中还提到 “在提交表单时校验全部的可见字段” 和 “只在提交成功时重置表单”,所以定义一个 “校验全部的可见字段” 的写操作器 validateAllVisibleFields
和一个 “重置表单” 的写操作器 reset
:
const validateAllVisibleFields = w((state: EnrollState) => {
const name = { ...state.name, error: validateName(state.name.value) };
// 根据 “填写身高” 选中情况校验 “身高”
const height = state.includingHeight.value
? { ...state.height, error: validateHeight(state.height.value) }
: state.height;
return { ...state, name, height };
});
const reset = w((_state: EnrollState) => enrollMug[construction]);
自定义读操作器
回到读操作上,在 React Mug 中可以 向帮助函数 r
中传入一个以目标状态为第一个参数的纯函数来自定义一个读操作器。
功能中提到 “在存在校验错误时提示修改,在不存在错误时提交数据”,所以定义一个检查是否 “存在校验错误” 的读操作器 hasAnyVisibleError
:
import { r } from 'react-mug';
const hasAnyVisibleError = r((state: EnrollState) => {
return !!(state.name.error || (state.includingHeight.value && state.height.error));
});
它的用法与 check
类似如下:
// 示例
const b = hasAnyVisibleError(enrollMug);
并且它也可以接收状态当纯函数调用:
// 示例
const b = hasAnyVisibleError(enrollState);
表单提交
现在着手实现提交表单的逻辑。
在 React Mug 中 强烈推荐直接使用编程语言自身的异步函数实现异步行为,因为这是最简洁的异步编程的方式。
这里定义一个异步函数 submitEnrollment
,然后分析一下大致包含以下步骤:
async function submitEnrollment() {
"校验全部的可见字段";
if ("存在校验错误") {
"提示修改";
"3 秒后清空修改提示";
return;
}
"清空全部提交相关提示";
"进入加载状态";
try {
"获取接口调用参数"
"调用接口";
"重置表单";
"提示成功";
"3 秒后清空成功提示";
} catch {
"提示失败";
"3 秒后清空失败提示";
}
"退出加载状态";
}
对应代码实现如下,其中在 swirl
调用中传入 none
能够置空对应的字段:
import { check, none, swril } from 'react-mug'
async function submitEnrollment() {
const mug = enrollMug;
validateAllVisibleFields(mug);
if (hasAnyVisibleError(mug)) {
swirl(mug, { submit: { hint: '请按照要求填写,谢谢' } });
setTimeout(() => swirl(mug, { submit: { hint: none } }), 3000);
return;
}
swirl(swirl(mug, { submit: { hint: none, success: none, failure: none } }), {
submit: { loading: true },
});
try {
const state = check(mug);
const postParams = state.includingHeight.value
? { name: state.name.value }
: { name: state.name.value, height: state.height.value };
await postEnrollment(postParams);
swirl(reset(mug), { submit: { success: '报名成功' } });
setTimeout(() => swirl(mug, { submit: { success: none } }), 3000);
} catch {
swirl(mug, { submit: { failure: '报名失败,请稍后再试' } });
setTimeout(() => swirl(mug, { submit: { failure: none } }), 3000);
}
swirl(mug, { submit: { loading: false } });
}
表单渲染
最后进入 React 空间,只需要写一个 React 组件 根据状态渲染界面 和 根据事件触发操作 就可以了,其中将 check
与 useOperator
结合调用能够在 React 组件中访问 Mug 最新状态:
import { check, swirl, useOperator } from 'react-mug';
function EnrollForm() {
const { name, includingHeight, height, submit } = useOperator(check, enrollMug);
return (
<form
onSubmit={(e) => {
e.preventDefault();
submitEnrollment();
}}
>
<div>
<label>姓名:</label>
<input
type="text"
value={name.value}
onChange={(e) => setName(enrollMug, e.target.value)}
/>
{name.error && <div>{name.error}</div>}
</div>
<div>
<label>填写身高:</label>
<input
type="checkbox"
checked={includingHeight.value}
onChange={(e) => swirl(enrollMug, { includingHeight: { value: e.target.checked } })}
/>
</div>
{includingHeight.value && (
<div>
<label>身高:</label>
<input
type="number"
value={height.value}
onChange={(e) => setHeight(enrollMug, e.target.value)}
/>
厘米
{height.error && <div>{height.error}</div>}
</div>
)}
<button type="submit" disabled={submit.loading}>
提交报名信息
</button>
{submit.hint && <div>{submit.hint}</div>}
{submit.success && <div>{submit.success}</div>}
{submit.failure && <div>{submit.failure}</div>}
</form>
);
}
export function App() {
return <EnrollForm />;
}
总结
在上面的案例中仅通过 150 行代码就以清晰的结构实现了一个功能全面的表单,一个 好用通用的状态库是可以快速解决表单问题的。
而且这种解决方式没有 “后患”,不必担心由于实际情况变得越来越复杂而无法招架。对软件和自身都是一种兼顾了短期和长期的投入。
希望大家喜欢,欢迎留言交流。
转载自:https://juejin.cn/post/7413940909262192676