4

前言

hooks是react16.8新增的特性。关于为什么要新增hooks,是因为class的组件会存在以下的一些问题。

  1. 在组件之间复用状态逻辑很难
  2. 复杂组件变得难以理解
  3. 难以理解的 class

这些点就不详细赘述了,这篇文章的重点是介绍hooks。

useState

function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

useState是用来取代class组件中的setState。useState接受一个值作为state的初始值,返回一个数组,数组第一个值是state,第二个值是一个用于修改state的函数。useState可以多次使用。栗子如下:

import React, { useState } from 'react';

const App = function() {
    const [count, setCount] = useState(0);
    // 另一个没有使用的state
    const [other, setOther] = useState('hello');
    
    return (
      <button onClick={() => setCount(count+1)}>{ count }</button>
    );  
}

它对应的class组件的代码如下:

import React, { Component } from 'react';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0,
            other: 'hello'
        };
    }

    setCount(count: number) {
        this.setState({
            count
        });
    }

    setOther(other: string) {
        this.setState({
            other
        });
    }

    render() {
        return (
            <button onClick={() => this.setCount(this.state.count + 1)}>
                {this.state.count}
            </button>
        );
    }
}

useReducer

useReducer和useState的用处是一样的,不同的是useReducer处理的是更加复杂的场景,例如三级联动选择框,需要同时对多个state进行联动处理。可以看做是一个小的redux,使用方式和redux也基本一致。

const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        default:
            return state;
    }
}

const App = function() {
    const [state, dispatch] = useReducer(reducer, { count: 0 });

    return (
        <>
            { state.count }
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    );
}

useEffect

function useEffect(effect: EffectCallback, deps?: DependencyList): void;
type EffectCallback = () => (void | (() => void | undefined));

Effect Hook 可以让你在函数组件中执行副作用操作。何为副作用操作呢?例如:请求接口、操作dom、定时器等等。我们可以使用useEffect模拟React中的一些生命周期函数。但需要注意的是:请不要把这种对应划上等号。

从他的定义中就可以知道,useEffect有两个参数,第一个参数是一个回调函数(可以返回一个函数或者不返回内容),第二个参数是依赖项。

模拟componentDidMount:

// 在didMount中请求一个接口
useEffect(() => {
    fetch('http://xxx.com/api/list');
}, []);

模拟componentDidUpdate

// 不传入依赖时,会在每次渲染之后执行
useEffect(() => {
    console.log('update');
});

模拟componentWillUnmount: useEffect的回调函数返回一个函数。

// 向document添加一个click事件,并在下次重新渲染前将其卸载。
useEffect(() => {
    const handler = () => { console.log('click'); }
    document.addEventListener('click', handler);

    return () => {
        document.removeEventListener('click', handler);
    }
}, []);

模拟shouldComponentUpdate: 这个我觉得应该和vue的watch更加相近。

// 在第一个加载和count发生变化时才会触发
useEffect(() => {
    // ...
}, [count]);

useMemo

如果你对vue比较熟悉的话,useMemo可以看做是computed,可以看做是需要手动添加依赖的计算属性,在依赖的值不发生改变时,返回的值是不变的。

const App = function() {
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');

    const name = useMemo(() => {
        return firstName + ' ' + lastName
    }, [firstName, lastName]);

    return (
        <>
            姓:<input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
            名:<input value={lastName} onChange={(e) => setLastName(e.target.value)} />
            姓名:{ name }
        </>
    );
}

useCallback

我们知道,函数组件中props或者state改变时,都会重新执行当前函数,那么如果我们直接在函数组件中定义处理函数的话,属性更改会触发修改,那么每次修改都会导致处理函数的重新定义,这样会造成极大的性能耗损。

useCallback便是用来处理这个问题,在依赖项不改变的情况下,函数不会重新定义。

const App = function() {
    const change = useCallback((e) => {
        console.log(e);
    }, []);

    return (
        <>
            <input onChange={change} />
        </>
    );
}

useRef

ref的作用我想很多前端小伙伴应该并不陌生,可以用来操作dom或者实例。而useRef除了用于操作DOM之外,在一些其他的方面也很有用,例如我们需要把一个定时器的值全局保存,但又不希望这个值的变化触发render,就像是我们在使用class组件时的this.timer = setInterval(...)

// 例如我们设置了定时获取数据的interval,在点击某个按钮之后就停止定时器
const App = () => {
    const inputEl = useRef(null); // 这个用来获取dom,绑定到input上之后,就可以通过inputEl.current进行访问
    const timer = useRef(null);

    useEffect(() => {
        timer.current = setInterval(...);
        return () => {
            clearInterval(timer.current);
        }
    }, []);

    return (
        <div>
            <button onClick={() => clearInterval(timer.current)}>Stop</button>
            <input ref={inputEl} />
        </div>
    );
}

useContext

从字面上也可以看出来,useContext就是为了方便使用context(一般用于祖孙组件的数据通信)的。需要注意的是调用了 useContext 的组件总会在 context 值变化时重新渲染。

const themes = {
    light: {
        color: '#fff',
        background: '#f12'
    }
};

const ThemeContext = createContext(themes.light);

const Child = () => {
    const theme = useContext(ThemeContext);

    return (
        <div style={{color: theme.color, background: theme.background}}>Child</div>
    );
}

const App = () => {
    return (
        <div>
            <ThemeContext.Provider value={themes.light}>
                <Child />
            </ThemeContext.Provider>
        </div>
    );
}

自定义Hook

自定义hook一般用来HOC做比较,他们都是用来对组件的逻辑进行复用。hook与HOC不同的是:

  1. 由于HOC机制的原因(包裹一层组件)会改变原组件的HTML结构,hook则不会;
  2. 使用过HOC的小伙伴应该都体验过使用了多个HOC之后,完全不知道某个prop是来自于哪里,也就我们常说的props来源不明。不方便调试;
  3. HOC可能会覆盖固有的props。

自定义hooks或使用hooks时需要注意的是:

  1. 自定义hook必须以use开头,这有利于react对自定义hook的规则进行检查;
  2. 不要在循环,条件或嵌套函数中调用 Hook;
  3. 不要在普通的 JavaScript 函数中调用 Hook。

栗子:在多个组件中需要实时获取鼠标的位置

// 定义hook
const useMousePosition = () => {
    const [location, setLocation] = useState({x: 0, y: 0});

    useEffect(() => {
        function move(e) {
            setLocation({ x: e.screenX, y: e.screenY });
        }

        document.addEventListener('mousemove', move);

        return () => {
            document.removeEventListener('mousemove', move);
        };
    }, []);

    return location;
}

// 使用hook
const App = () => {
    const position = useMousePosition();

    return (<div>{ position.x } { position.y }</div>);
}

可以看到,我们的就对代码进行了复用,也避免了上述关于HOC的问题。

小结

hooks的引入让我们很方便的使用函数组件来编写代码,不过关于OOP和FP的争议也一直存在,所以是否使用hooks也需要童鞋们好好考量。

参考:React Hook API 索引


WillemWei
491 声望37 粉丝