1. 使用 useReducer hook
useReducer
是 useState
的替代品,它可以更好的管理组件的状态。
useReudcer
的格式:
import { useReducer } from "react";
let [state, dispatch] = useReducer(reducer, initialArg, init);
各个变量的含义:
state
拿到状态数据;dispatch
派发 action 的函数;reducer
我们自己编写的reducer
函数;initialArg
初始化的 state 值;init
惰性初始化函数,该函数的参数是我们传入的第二个initialArg
参数,这么做可以将用于计算state
的逻辑提取到reducer
外部。
initialArg
和 init
都是可选参数。useReducer
的工作原理与 redux 有些相似,useReducer 返回的数组的第二个参数就像 redux 中的 dispatch
,可以派发 action
。
下面是一个计时器功能的例子:
代码语言:javascript复制import React,{ useReducer, useCallback } from "react";
// reducer 函数
function reducer(state, action){
const { type, payload } = action;
switch(type){
case "add":
return state payload;
case "minus":
return state - payload;
default: return state;
}
}
function App(){
// 使用 useReducer
let [state, dispatch] = useReducer(reducer, 0);
const handleAddClick = useCallback(() => {
dispatch({
type: "add",
payload: 1
});
},[]);
const handleMinusClick = useCallback(() => {
dispatch({
type: "minus",
payload: 1
});
},[]);
return (
<div>
<h1>{state}</h1>
<button onClick={handleAddClick}>add</button>
<button onClick={handleMinusClick}>minus</button>
</div>
);
}
export default App;
如果你习惯在 reducer 中定义初始值,可以这么做:
代码语言:javascript复制function reducer(state = 0, action = {type: "@@INIT"}){
const { type, payload } = action;
switch(type){
case "add":
return state payload;
case "minus":
return state - payload;
default: return state;
}
}
action 需要有一个初始的 type,不然会报错,这就像 redux 内部定义了一个初始 action 一样。
手写一个 useReducer
下面的代码是一个简化版的 useReducer 钩子函数:
代码语言:javascript复制function useReducer(reducer, initialState){
let [state, setState] = useState(initialState);
function dispatch(action){ // 派发 action
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
2. immer 工具库
在编写 react redux 应用时,reducer
中的 state 如果是一个引用类型,比如数组或者对象,当往数组中 push
新的项时,我们必须要克隆一份才行,如果不克隆,react 会认为 state 并没有更新。
function reducer(state = [], action){
const { type, payload } = action;
switch(type){
case "increase":
[...state, payload];
default: return state;
}
}
如果数据很复杂时,克隆难度就会加大,扩展运算符也只是浅克隆,而使用 JSON.parse
、JSON.stringify
是很费性能的,它的效率不高。
immer
库就是为了解决这个问题的。它是 mbox
库的作者的另一个作品,与 mobx
一样简单易用。
使用如下:
代码语言:javascript复制import produce from "immer"
const baseState = [
{
todo: "Learn typescript",
done: true
},
{
todo: "Try immer",
done: false
}
];
const nextState = produce(baseState, draftState => {
// 直接对数据进行修改(draftState 是克隆后的数据)
draftState.push({todo: "Tweet about it"})
draftState[1].done = true
})
produce
函数接收原始的 state 数据,它会把这个数据深度克隆,然后把克隆后的 state 传递给回调函数,我们在回调函数里就可以进行 push
操作了!最终 produce 会返回操作后的新的 state。
甚至可以直接用 produce
函数包裹 reducer
:
const reducer = produce((state, action) => {
// 这个 state 是被克隆的 state
const { type, payload } = action;
switch(type){
case "add":
state.push(payload);
// 操作后返回 state
return state;
case "minus":
state.pop(payload);
return state;
default: return state;
}
});
如果要初始化 state,可以将初始值放在 produce 函数的第二个参数上:
代码语言:javascript复制const reducer = produce((state, action) => {
// 这个 state 是被克隆的 state
const { type, payload } = action;
switch(type){
default: return state;
}
},["hello!"]);
useImmer
useImmer 是一个 React Hook,使用时需要先下载:
代码语言:javascript复制npm install immer use-immer -S
use-immer 包有两个 Hook:useImmer
和 useImmerReducer
。它们分别对应 React 当中的 useState
和 useReducer
。
例如:
代码语言:javascript复制import { useImmer } from "use-immer";
function App(){
// 使用 useImmer 钩子
let [list, updateList] = useImmer(["你好!"]);
let [msg, setMsg] = React.useState("");
const handleAddClick = useCallback(() => {
// 调用 updateList,draft 是经过深度拷贝后的 state 数组
updateList(draft => {
draft.push(msg);
});
setMsg("");
},[msg]);
const handleMinusClick = useCallback(() => {
// 调用 updateList,draft 是经过深度拷贝后的 state 数组
updateList(draft => {
draft.pop();
})
},[]);
const handleChange = useCallback((e) => {
setMsg(e.target.value);
},[]);
}
useImmerReducer
接收两个参数:reducer
和 initialState
。返回的同样是 state
和 dispatch
。
function reducer(draft, action){
// draft 是深度克隆后的 state
const { type, payload } = action;
switch(type){
case "add":
draft.push(payload);
return draft;
case "minus":
draft.pop();
return draft;
default: return draft;
}
}
function App(){
// 使用 useImmerReducer
let [list, dispatch] = useImmerReducer(reducer ,["你好!"]);
let [msg, setMsg] = React.useState("");
const handleAddClick = useCallback(() => {
// 派发 action
dispatch({
type: 'add',
payload: msg
});
setMsg("");
},[msg]);
const handleMinusClick = useCallback(() => {
dispatch({
type: 'minus'
});
},[]);
const handleChange = useCallback((e) => {
setMsg(e.target.value);
},[]);
}
比起来 immutable.js
库,个人认为 immer
比它好用太多,immer
提供的 API 很少,immutable
书写起来比较繁杂,API 众多,而且很多都是重复性代码。而 immer
轻量、简洁、易上手、并且使用起来也非常的舒服,不会产生容易把 immutable 数据类型与原生 JS 数据类型搞混的情况。
3. Formik 工具库
Formik
库可以让你在 React 中轻松构建出健壮的 Form 表单程序。使用时需要先下载:
npm install formik --save
Formik
库可以与 yup
库一块使用,库的作者也推荐搭配使用,yup
是一个用于验证字段的库,它的用法类似于 React 中的 PropTypes
。yup
库使用之前也需要先下载。
用法
下面写个例子,一个表单,我们需要表单做验证,验证不通过就提示用户为什么不对。需要验证的字段:
nickname
昵称,最少 1 位,首尾不能有空格符,最多 30 位;email
邮箱,需要符合邮箱格式;password
密码,最小 6 位,最大 30 位;password
确认密码,应与上面的密码一致;gender
性别,可选的单选框;age
年龄,可选填;
Formik
库提供了几个表单组件:
<Field />
相当于增强版的input
标签(它也可以表示别的表单组件),在使用时,也应设置如type
、name
等属性。它有一个as
属性,值可以是 React 组件,也可以是要呈现的 HTML 元素的名称。例如:
// Field 作为 select 标签使用
<Field as="select" name="color">
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</Field>
<ErrorMessage />
有一个name
属性,表示你把该组件与哪个表单控件绑定,当那个表单控件有错误时(验证失败),<ErrorMessage />
可以用来展示错误消息。<Formik />
用于构建表单的组件。用于集中处理表单逻辑。
<Formik />
组件比较复杂,在构建 Formik
表单程序时,Formik
和下面它的几个属性是需要设置的:
initialValues
接收一个对象,表示初始化的表单控件的值,对象的键应是表单的name
值;<Formik />
的children
部分可以是一个函数,这个函数可以接收到<Formik />
的porps
;<Form />
对form
表单的小小封装,<Form />
组件可以让你不用再手动创建onSubmit
或onResize
事件句柄,在Formik
组件中直接书写即可。
下面开始编写代码。
页面大致长这样:
formik
代码:
代码语言:javascript复制import React from 'react';
import { Formik, Form, Field, ErrorMessage } from "formik";
import * as Yup from "yup";
// 字段名应与表单元素的 name 值相同
const initialValues = {
nickname: "",
email: "",
password: "",
reTypePassword: "",
gender: "",
age: 0,
};
const Test = () => {
return (
<div className="wrapper">
<Formik
// 初始化的字段值
initialValues={initialValues}
validationSchema={FormSchema} // 验证函数
// 当失去焦点时,不触发验证,只有 change 事件发生时才触发
validateOnBlur={false}
// 提交时就打印出各个字段(action 是 Formik 中的一些方法)
onSubmit={(values, action) => console.log(values, action)}
>
<Form method="GET" action="/">
{/* 在 span 中展示的是验证不通过时的提示 */}
<span className="warning"><ErrorMessage name="nickname" /></span><br />
<Field type="text" placeholder="昵称" name="nickname" /><br /><br />
<span className="warning"><ErrorMessage name="email" /></span><br />
<Field type="email" placeholder="邮箱" name="email" /><br /><br />
<span className="warning"><ErrorMessage name="password" /></span><br />
<Field type="password" placeholder="密码" name="password" /><br /><br />
<span className="warning"><ErrorMessage name="reTypePassword" /></span><br />
<Field type="password" placeholder="确认密码" name="reTypePassword" /><br /><br />
<label>
<Field type="radio" name="gender" value="male" />男
</label>
<label>
<Field type="radio" name="gender" value="female" />女
</label><br /><br />
<span className="warning"><ErrorMessage name="age" /></span><br />
<Field type="number" name="age" /> 年龄<br /><br />
<Field type="submit" id="form-submit-btn" value="提交" />
</Form>
</Formik>
</div>
);
}
export default Test;
CSS 代码:
代码语言:javascript复制input:not([type="radio"]):not([type="submit"]){
width: 300px;
height: 28px;
padding-left: 10px;
font-size: 16px;
letter-spacing: 2px;
}
.wrapper form input[type="number"]{
width: 64px;
}
form span.warning{
color: red;
padding-left: 6px;
font-size: 14px;
}
input[type="radio"]{
height: 18px;
width: 18px;
vertical-align: bottom;
}
input[type="submit"]{
width: 120px;
height: 36px;
cursor: pointer;
}
下面是我们自己定义的 FormSchema
:
const FormSchema = Yup.object().shape({
nickname: Yup.string().trim() // 去掉前后的空白字符
.min(1, "昵称不能少于 1 位")
.max(30, "昵称太长了!")
.required("昵称还没填写呢~"), // required 表示必填项
email: Yup.string().email("无效的邮箱")
// test 函数内部还可以异步的验证字段,test 的第一个参数是测试名称,你可以传入一个字符串
.test("Is it registered", "邮箱已经被注册", async (value) => {
let res = await fetch(`/api/test/email?email=${value}`);
let data = await res.json();
// test 返回的结果是 false 时,会有验证失败提示
return data.msg !== 1; // 1 表示已经被注册
}).required("请填写邮箱"),
password: Yup.string()
.test('legal password', "不能包含 > < : ( ) 空格 \ / 字符", value => !(/>|<|:|(|)|s|\|//.test(value)))
.min(6, "密码至少六位")
.max(30, "密码长度不应多于 30 位")
.required("请填写密码"),
reTypePassword: Yup.string()
.when('password', (password, schema) => {
// 用 when 可以拿到 password 字段值,然后进行测试,如果两个值相等,说明可以,不然提示不对
return schema.test('verify consistency', "两次密码不一致", value => value === password);
}).required("密码不能为空"),
age: Yup.number().integer("必须是一个整数")
.min(0, "无效的年龄")
.max(200, "无效的年龄")
});
上面的汉字内容都是当验证不通过时,提醒用户的信息,这些信息会映射到 ErrorMessage
组件中,然后展示出来。使用 Formik
yup
库实现了验证逻辑与组件的解耦,验证逻辑统一由 yup
管理。
相对于 redux-form
库,我觉得 formik
库更好用一些吧。在 Formik
官网,作者也举例了使用 redux-form
的缺陷:
- 表单状态本质上是短暂的和局部的,并不需要
redux
对其进行跟踪; - 使用
redux
管理状态时,状态更新要派发 action,这对于小型应用程序来说很好,但是随着 Redux 应用程序的增长,使用Redux-Form
,则输入延迟将继续增加。用户体验就不太好了。 redux-form
库比较大,压缩后大小为22.5KB
,而Formik
库为12.7KB
;
关于 formik
的更多用法,可以参考官网:
Formik.js[1]
yup.js[2]
参考资料
[1]
Formik.js: https://jaredpalmer.com/formik/docs/overview
[2]
yup.js: https://github.com/jquense/yup