React Hooks

19 5月

React v16.7.0-alpha中提供了hooks功能,目前处于提案阶段。

  • 概述
  • useState
  • useEffect
  • 基本规则
  • useContext,useReducer,useMemo,useCallback,useRef,useImperativeMethods,useLayoutEffect

概述

React定义一个组件最简单的方式是使用JavaScript函数,也被称为函数式组件:

// 函数式组件
function Welcome(props) {
    return <h1>Hello, {props.name}</h1>;
} 

// 类组件
class Welcome extends React.Component { 
    render() { 
        return <h1>Hello, {this.props.name}</h1>; 
    } 
}

函数式组件不能定义state,不能使用生命周期方法,所以适合无状态纯UI展现的组件。

一种说法是函数式组件的代码量少(类本质是语法糖,会额外有一些转义代码)能提升性能。我表示存疑,减少这么一丢丢代码,能提升多少?没有双盲实验的数据分析都是YY,这点大小上的差别是造成性能不佳的瓶颈吗?

函数式组件存在的意义更多体现在编程理念上。因为没有生命周期,所以父组件刷新时,它一定会刷新,没有state,导致它和数据处理彻底解耦,所以它是个彻底的UI展现组件。

耐不住寂寞FB推出了hooks,允许你在函数式组件里hook住state和生命周期。

useState

const [state, setState] = useState(initialState);:参数initialState只用于第一次渲染,之后就没什么卵用了,即第一次渲染后,返回值state和initialState相同。

如果初始状态比较复杂,可用函数来实现懒初始化(Lazy initialization):

const [state, setState] = useState(() => {
    const initialState = someExpensiveComputation(props);
    return initialState;
});

返回值是成对的:state和更新它的函数setState。类组件中用this.state.count访问,函数式组件中用hook创建的state不用this,可以直接读取。通常,当函数退出时变量就销毁了,但hook创建的state,React会替我们保留。setCount支持两种方式:

// 方式一
setCount(newState);

// 方式二
setState(prevState => { 
    return {...prevState, ...updatedValues};
});

useEffect

useEffect(effect[, [values]]);:参数effect是个函数,用于执行副作用。可以理解为将componentDidMount,componentDidUpdate,componentWillUnmount生命周期统一成了一个API,React在每次渲染后会自动运行effect。effect因为在组件内声明,所以可以访问props,state。

有些副作用的动作,在不同生命周期中要区分对待,例如在componentDidMount中订阅,在componentWillUnmount中取消订阅。可以让effect返回一个函数,React将在清理时运行它:

useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);  // 订阅
    return function cleanup() {                                            // React将在清理时运行取消订阅
        ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
});

这里的清理,不仅指componentWillUnmount。effect默认每次渲染后都会触发,而且始终会创建新的effect以替换旧的effect,所以这里的清理,包括每次清理旧的effect,参照下面的时间序列:

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // Run first effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // Run next effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // Run next effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect

但每次创建新的effect以替换旧的effect会带来不必要的性能问题。例如订阅时,仅当属性改变时才订阅,不用每次渲染后都重新创建新订阅。你需要第二个参数,它是effect依赖的值数组:

useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
        ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
}, [props.friend.id]);

最后如果第二个参数传递空数组[],相当于告诉React你的effect仅在mount时运行,并在unmount时执行清理,永远不会在update后运行。

完整点的例子:

import React, { useState, useEffect } from 'react';

export default function Example() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        document.title = `点了 ${count} 次`;
    });
      
    return (
        <>
            <p>点了 {count} 次</p>
            <button onClick={() => setCount(count + 1)}>点我</button>
            <button onClick={() => setCount(0)}>重置</button>
        </>
    );
}

基本规则

hook有两个规则,你可以用eslint-plugin-react-hooks插件来帮助你检查代码中是否违反了这两个规则:

  • 只能在顶层调用Hook,不要在循环,条件或嵌套函数中调用Hook。
  • 仅在React函数式组件中调用Hook,不要在普通JS函数调用Hook。

因为React调用hook时,依赖它们间的顺序,例如:

function Form() {
    const [name, setName] = useState('Mary');

    useEffect(function persistForm() {
        localStorage.setItem('formData', name);
    });

    const [surname, setSurname] = useState('Poppins');
  
    useEffect(function updateTitle() {
        document.title = name + ' ' + surname;
    });

    ...
}

上述代码的调用顺序是固定的:

// 第一次渲染
useState('Mary')           // 1. 用'Mary'初始化 name
useEffect(persistForm)     // 2. 将 name 保存进 localStorage
useState('Poppins')        // 3. 用 'Poppins' 初始化 surname
useEffect(updateTitle)     // 4. 将 surname 更新到 title 上

// 第二次渲染
useState('Mary')           // 1. 读取当前 name(忽略参数)
useEffect(persistForm)     // 2. 将 name 保存进 localStorage
useState('Poppins')        // 3. 读取当前 surname(忽略参数)
useEffect(updateTitle)     // 4. 将 surname 更新到 title 上

如果打破第一条规则,将hook放置在条件语句中,会导致顺序错乱,导致bug。应该将hook放在顶层:

// 错误的方式:
if (name !== '') {
    useEffect(function persistForm() {
        localStorage.setItem('formData', name);
    });
}
----
useState('Mary')           // 1. 读取 name  状态变量(忽略参数)
// useEffect(persistForm)  // 这个Hook被跳过了
useState('Poppins')        // 2 (之前是 3). 读取 surname 失败
useEffect(updateTitle)     // 3 (之前是 4). 将 surname 更新到 title 上失败
----

// 正确的方式:
useEffect(function persistForm() {
    if (name !== '') {
        localStorage.setItem('formData', name);
    }
});

其他API

useContext

const context = useContext(Context);:可以获取context对象,即React.createContext返回的值

useReducer

const [state, dispatch] = useReducer(reducer, initialState);:它是useState的替代方案。参数reducer就是Redux里的reducer,即(state, action) => newState。返回值是state和dispatch。例如:

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
    switch (action.type) {
        case 'reset':
            return initialState;
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        default:
            return state;
    }
}

function Counter({ initialCount }) {
    const [state, dispatch] = useReducer(reducer, { count: initialCount });
    return (
        <>
            Count: {state.count}
            <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
        </>
    );
}

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);:只会在其中一个输入发生变化时才重新计算memoized值并返回,有助于避免在每个渲染上进行高开销的计算。

useCallback

const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);:等价于useMemo,区别是返回一个memoized回调函数。

useRef

const refContainer = useRef(initialValue);:可以在函数式组件内使用ref

function TextInputWithFocusButton() {
    const inputEl = useRef(null);

    const onButtonClick = () => {
        inputEl.current.focus();
    };
    
    return (
        <>
            <input ref={inputEl} type="text" />
            <button onClick={onButtonClick}>聚焦</button>
        </>
    );
}

useImperativeMethods

useImperativeMethods(ref, createInstance, [inputs]):和forwardRef一起使用

function FancyInput(props, ref) {
    const inputRef = useRef();

    useImperativeMethods(ref, () => ({
        focus: () => {
            inputRef.current.focus();
        }
    }));

    return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

// 渲染 <FancyInput ref={fancyInputRef} /> 的父组件将能够调用 fancyInputRef.current.focus() 

useLayoutEffect

签名同useEffect,但在所有DOM变化后同步触发。使用它来从DOM读取layout并重新渲染。

最后,hooks还很新,并没有在项目中大量尝试,形成很深的体会。我更在乎的是开发效率和日常维护,最好所有代码都是最直白的if-else,让阅读代码的人留个神,脑中过个弯的都不是好代码。但不妨碍我们持续关注并学习这些新特效。

评论(1)

发表评论

电子邮件地址不会被公开。 必填项已用*标注