React 组件优化

2020-04-27 10:18:24 浏览数 (1)

1. 使用 useReducer hook

useReduceruseState 的替代品,它可以更好的管理组件的状态。

useReudcer 的格式:

代码语言:javascript复制
import { useReducer } from "react";
let [state, dispatch] = useReducer(reducer, initialArg, init);

各个变量的含义:

  • state 拿到状态数据;
  • dispatch 派发 action 的函数;
  • reducer 我们自己编写的 reducer 函数;
  • initialArg 初始化的 state 值;
  • init 惰性初始化函数,该函数的参数是我们传入的第二个 initialArg 参数,这么做可以将用于计算 state 的逻辑提取到 reducer 外部。

initialArginit 都是可选参数。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 并没有更新。

代码语言:javascript复制
function reducer(state = [], action){
    const { type, payload } = action;
    switch(type){
        case "increase":
            [...state, payload];
        default: return state;
    }
}

如果数据很复杂时,克隆难度就会加大,扩展运算符也只是浅克隆,而使用 JSON.parseJSON.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:

代码语言:javascript复制
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:useImmeruseImmerReducer。它们分别对应 React 当中的 useStateuseReducer

例如:

代码语言: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 接收两个参数:reducerinitialState。返回的同样是 statedispatch

代码语言:javascript复制
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 表单程序。使用时需要先下载:

代码语言:javascript复制
npm install formik --save

Formik 库可以与 yup 库一块使用,库的作者也推荐搭配使用,yup 是一个用于验证字段的库,它的用法类似于 React 中的 PropTypesyup 库使用之前也需要先下载。

用法

下面写个例子,一个表单,我们需要表单做验证,验证不通过就提示用户为什么不对。需要验证的字段:

  • nickname 昵称,最少 1 位,首尾不能有空格符,最多 30 位;
  • email 邮箱,需要符合邮箱格式;
  • password 密码,最小 6 位,最大 30 位;
  • password 确认密码,应与上面的密码一致;
  • gender 性别,可选的单选框;
  • age 年龄,可选填;

Formik 库提供了几个表单组件:

  • <Field /> 相当于增强版的 input 标签(它也可以表示别的表单组件),在使用时,也应设置如 typename 等属性。它有一个 as 属性,值可以是 React 组件,也可以是要呈现的 HTML 元素的名称。例如:
代码语言:javascript复制
// 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 /> 组件可以让你不用再手动创建 onSubmitonResize 事件句柄,在 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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
                    <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

代码语言:javascript复制
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

0 人点赞