React Hooks你真的会用了吗?

603

hooks组件排列

先来举个反例,未做优化的写法

import React, { useState } from 'react';

const Header = ({ title }) => {
    console.log('header render')
    return (
        <div>
            {title}
        </div>
    )
}

const Footer = () => {
    console.log('footer render')
    return (
        <div>
            页底
        </div>
    )
}

export default function Home() {

    const [num, setNum] = useState(0);

    console.log('render')

    return (
        <div>
            <Header title={'标题头部'} />
            <div>
            	<span>{num}</span>
            	<button onClick={() => setNum(v => v + 1)}>点击</button>
        		</div>
            <Footer />
        </div>
    );
}

这里当点击按钮以后会引起num变化从而重新渲染headerfooter两个子组件,造成多余的render性能损耗。

正确写法之一(将组件通过memo或者useMemo进行包裹)

import React, { useState, useMemo, memo } from 'react';

const Footer = memo(() => {
    console.log('footer render')
    return (
        <div>
            页底
        </div>
    )
})

const Head = ({ title }) => {
    console.log('header render')
    return (
        <div>
            {title}
        </div>
    )
}

export default function Home() {

    const [num, setNum] = useState(0);

    console.log('render')

    const Header = useMemo(() => <Head title={'标题头部'} />, [])

    return (
        <div>
            {Header}
            <div>
                <span>{num}</span>
                <button onClick={() => setNum(v => v + 1)}>点击</button>
            </div>
            <Footer />
        </div>
    );
}

我们都知道react的组件会根据 state和props属性(父组件传递的) 的变化而发生重新re-render渲染,这里用memo或者useMemo包裹以后,会自动帮我们做一个浅比较,避免造成不必要的渲染。

正确写法之二(思想都是互通的,就是尽可能减少组件的不必要渲染而少做事,都拆分成独立子组件)

import React, { useState } from 'react';

const Header = ({ text }) => {
    console.log('header render')
    return (
        <div>
            {text}
        </div>
    )
}

const Footer = () => {
    console.log('footer render')
    return (
        <div>
            页底
        </div>
    )
}

const Content = () => {
    const [num, setNum] = useState(0);

    console.log('content render')

    return (
        <div>
            <span>{num}</span>
            <button onClick={() => setNum(v => v + 1)}>点击</button>
        </div>
    )
}

export default function Home() {

    console.log('render')

    return (
        <div>
            <Header text={'标题头部'} />
            <Content />
            <Footer />
        </div>
    );
}

合理使用useReducer去替代useState

有时候我们会遇到一个场景,在一个函数里需要setState两到三次,但是Hooks中的setState 与 Class 中最大区别在于Hooks的setState会覆盖原本的值,所以需要用扩展符

其中一个解决方案是合并state操作,如以下

	//伪代码
   const [param,setParam] = useState({
          name:'',
          age:''
   })
       
   const onChange = () => {
   		setParam(state => ({
        	...state,
            name:'Alan',
            age:20
        }))
   }

但是这样的做法不是万能解,当遇到要处理复杂逻辑时可以考虑用useReducer了,举个需要实现功能的例子: 一个简易多选框的实现,当其他选项被选中时,主选项要被选中,主选项可以控制其他选项是否被选中。

先来看下我的不规范写法

// 伪代码
const [checkBoxParam, setCheckBoxParam] = useState({
    //主选项和次选项
    primaryCheckBox: false,
    secondCheckBox: []
})

// 所有checkbox的id
const allCheckBoxId = [1, 2, 3]

//选择次选项时
const onHandleClick = (checkBoxArray = []) => {
    const { primaryCheckBox, secondCheckBox } = checkBoxParam;
    //如果次选项选满的情况
    if (checkBoxArray.length === allCheckBoxId.length) {
        setCheckBoxParam(param => ({
            ...param,
            primaryCheckBox: true,
            secondCheckBox: allCheckBoxId
        }))
    } else {
        setCheckBoxParam(param => ({
            ...param,
            primaryCheckBox: false,
            secondCheckBox: checkBoxArray
        }))
    }
}

//选择主选项时
const onPrimaryClick = (bool) => {

    if (bool) {
        setCheckBoxParam(param => ({
            ...param,
            primaryCheckBox: true,
            secondCheckBox: allCheckBoxId
        }))
    } else {
        setCheckBoxParam(param => ({
            ...param,
            primaryCheckBox: false,
            secondCheckBox: []
        }))
    }
}

看了上面的代码,可以拆分出三种情况:

  1. 次选项未选满时
  2. 次选项被选满或者主选选项被选中
  3. 主选选择被反选

根据上面三种情况可以拆分成switch case的条件判断,由此来分析就可以用useReducer了

优化以后

//伪代码

const checkBoxReducer = (state, action) => {

    switch (action.type) {
        //次选项未选满
        case 'second_check':
            return { ...state, checkedPriceList: action.payload, primaryCheckBox: false };
        //次选项被选满或者主选选项被选中
        case 'all_check':
            return { ...state, checkedPriceList: action.payload, primaryCheckBox: true };
        //主选选择被反选
        case 'un_all_check':
            return { ...state, checkedPriceList: action.payload, primaryCheckBox: false };
        default:
            throw new Error();

    }

}


const [checkBoxParam, dispatchCheckBoxParam] = useReducer(checkBoxReducer, {
    //主选项和次选项
    primaryCheckBox: false,
    secondCheckBox: []
})

// 所有checkbox的id
const allCheckBoxId = [1, 2, 3]

//选择次选项时
const onHandleClick = (checkBoxArray = []) => {

    //如果次选项选满的情况
    if (checkBoxArray.length === allCheckBoxId.length) {
        dispatchCheckBoxParam({ type: 'all_check', payload: allCheckBoxId });
    } else {
        dispatchCheckBoxParam({ type: 'second_check', payload: checkBoxArray });
    }
}

//选择主选项时
const onPrimaryClick = (bool) => {

    if (bool) {
        dispatchCheckBoxParam({ type: 'all_check', payload: allCheckBoxId });
    } else {
        dispatchCheckBoxParam({ type: 'un_all_check', payload: [] });
    }
}

使用useCallback缓存函数

useCallback主要用来作缓存函数,与useMemo的思想一样,根据依赖值的前后变化来判断函数是否被更新

function Parent(props) {
  const [count, setCount] = useState(0);
  // 使用 useCallback 缓存函数,当count对比前后发送变化时才改变函数
  const addClick = useCallback(() => {
    let sum = 0;
    for (let i = 0; i < count; i++) {
      sum += i;
    }
    return sum;
  }, [count]);
  const [value, setValue] = useState("");
  return (
    <div>
      <h3>UseCallbackPage</h3>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>add</button>
      <input value={value} onChange={event => setValue(event.target.value)} />
      {/*将 使用 useCallback 缓存后的函数传递给子组件,可避免子组件不必要的更新*/}
      <Child addClick={addClick} />
    </div>
  );
}
const Child = ({addClick}) => {
 
    console.log("child render");

    return (
      <div>
        <h3>Child</h3>
        <button onClick={() => console.log(addClick())}>add</button>
      </div>
    );
  
}

在上面的代码中,使用 useCallbackaddClick 函数做了缓存处理,然后把 addClick 作为props 传递给 Child 组件,当 Parent 组件更新时,就避免了 Child 组件的更新。

合理使用hooks组合思想(自定义校验表单)

先来说下使用场景需求,由于小程序的Form表单组件不好使用和修改,于是就直接写了input、checkbox等原始组件,但是这个时候涉及到多值的空校验和正则校验,处理不好的话代码就会写得比较杂乱。

import React, {useCallback,useState} from 'react';
import _isEmpty from 'lodash/isEmpty';
import _debounce from 'lodash/debounce';

//需要校检的选项列表
const validatorList = [
    {
        value: 'phone',
        checkFunc: value => new RegExp(/^[1][3,5,7,8][0-9]{9}$/).test(value),
        msg: '请填写正确格式的手机号码'
    },
    {
        value: 'code',
        checkFunc: value => !_isEmpty(value.trim()),
        msg: '请填写验证码'
    }
];

//input框防抖
const handleInput = _debounce((value, handle) => {
    handle(value);
}, 500);

const LoginComponent = () => {

    const [phone, setPhone] = useState();
    const [code, setCode] = useState();

    //收集表单的值
    const getFormValue = useCallback(() => {
        const data = {
            phone, code
        };
        return data;
    }, [phone, code])

    //校验表单每项的值
    const checkFormValue = useCallback(() => {
        const formValue = getFormValue();
        let checkStatus = true;
        for (let index = 0; index < validatorList.length; index++) {
            const rule = validatorList[index];

            if (!rule.checkFunc(formValue[rule.value])) {
                message.error(rule.msg);
                checkStatus = false;
                break;
            }

        }
        return checkStatus;
    }, [getFormValue])

    //提交表单
    const handleSubmit = useCallback(() => {

        if (!checkFormValue()) {
            return;
        }

        const data = getFormValue();

        axios.post('/api', data)
            .then(res => console.log('返回数据:' + res))
            .catch(err => console.log('返回数据失败: ' + err))
    }, [checkFormValue, getFormValue])

    return (

        <div>
            <input
                name='phone'
                value={phone}
                onchange={v => handleInput(v, setPhone)}
            />
            <input
                name='code'
                value={code}
                onchange={v => handleInput(v, setCode)}
            />
            <button onClick={handleSubmit}> 确定 </button>
        </div>
    )

}