前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React 组件优化

React 组件优化

作者头像
多云转晴
发布2020-04-27 10:18:24
7.2K0
发布2020-04-27 10:18:24
举报
文章被收录于专栏:webTowerwebTower

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebTower 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 使用 useReducer hook
    • 手写一个 useReducer
    • 2. immer 工具库
      • useImmer
      • 3. Formik 工具库
        • 用法
          • 参考资料
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档