React Hook初识

168 阅读6分钟

什么是React Hook(What)

Hook 是React 16.8的新增特性,它可以让你在不编写class的情况下,使用state以及其他React特性。

Hook是什么

Hook是一个特殊的行数,它可以让你“钩入”React的新特性,如 useState 就是允许你在React函数组件中添加state的Hook。

为什么要用React Hook(Why)

函数组件不能满足需求的时候,比如需要使用到state,或者需要一个生命周期的环境的时候,就可以考虑用Hook来满足需求。就像使用React Redux一样,当你需要使用的时候,就会想起来的了。

怎么用React Hook(How)

首先来了解Hook有哪一些:

  • 基础的Hook

    useState, useEffect, useContext

  • 额外的Hook

    useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, useDebugValue

useState

const [state, setState] = useState(initState);
// e.g
const [count, setCount] = useState(0)
// 可以传入一个函数,在函数中计算出state的值并返回,但是,只会在初始渲染时被调用
const [count, setCount] = useState(() => {
    const initState = someExpensiveComputation(props);
    return initState;
})

这是数组解耦的方式赋值,由此可见,useState返回了一个 state 以及更新这个 state 的函数。

setState(newState);
// e.g
setCount(1);

setState 用于更新 state。接收一个新的 state 并将组件的一次重新渲染加入队列。

注意

useState 不会自动合并更新对象。

useEffect

在函数组件的主体内(React渲染阶段),改变DOM,添加订阅,设置定时器,记录日志以及执行其他包含副作用的操作是不被允许的,这将导致莫名其妙的bug并破坏UI的一致性。

useEffect为纯函数组件创造了一个有类似于生命周期的环境。

useEffect(didUpdate);
// e.g
useEffect(() => {
    const subscription = props.source.subscribe();
    // 返回一个清除函数
    return () => {
        subscription.unsubscribe()
    };
})

执行的时机:

赋值给 useEffect 的函数组件会在组件渲染到屏幕之后执行。默认情况下,每一轮渲染结束后都会执行。

当有传参的时候,将会考虑参数情况来进行渲染:

useEffect(() => {
    const subscription = props.source.subscribe();
    return () => {
        subscription.unsubscribe()
    };
}, [props.source])

useEffect(() => {
    const subscription = props.source.subscribe();
    // 返回一个清除函数
    return () => {
        subscription.unsubscribe()
    };
}, [])

第一种情况,当 props.source 变化的时候也就是effect依赖变化的时候,effect会重新创建

第二种情况,是说明了effect不依赖于 props 或 state,它只会执行一次,永远不需要重复执行

注意

在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟执行。如果需要同步UI更新,可以使用useLayoutEffect。useLayoutEffect 结构与 useEffect 相同,只是调用时机不同

useContext

在了解useContext之前,插播一下 context:

Context 提供了一个无须为每层组件手动添加props,就能在组件树间进行数据传递的方法。

何时使用 Context:

当组件需要一个“全局”的数据,比如,当前认证的用户,主题,语言等。

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}

使用Context来改造

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

更多Context的信息,可以查看Context

useContext:

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定

useContext(MyContext) 相当于class组件中的 static ContextType = MyContext 或者 <MyCountext.provider.Consumer>

useReducer

待学习

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

在class组件,有 shouldComponentUpdate 可以判断是否需要更新,但是在函数组件内,没有此方法,因此每一次调用都会执行内部的所有逻辑,性能损耗比较严重。useCallback 与 useMemo是解决此问题的杀手锏。它们都会在组件第一次渲染的时候执行,useMemo返回缓存的变量,useCallback返回缓存的函数。

代码参考于useMemo与useCallback使用指南

export default function WithoutMemo() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
 
    function expensive() {
        console.log('compute');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }
 
    return <div>
        <h4>{count}-{val}-{expensive()}</h4>
        <div>
            <button onClick={() => setCount(count + 1)}>+c1</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
}

无论是修改count还是val,组件都会重新渲染,触发expensive的执行,但 expensive 的计算应该是依赖于 count的变化才对。因此可以使用useCallback来改善:

export default function WithoutMemo() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
 
    const expensive = useCallback(() => {
        console.log('compute');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }, [count])
 
    return <div>
        <h4>{count}-{val}-{expensive()}</h4>
        <div>
            <button onClick={() => setCount(count + 1)}>+c1</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
}

useMemo

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

useRef

ref对象是一个通用容器,其current属性是可变的。

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

在本例中,渲染 <FancyInput ref={fancyInputRef} /> 的父组件可以调用 fancyInputRef.current.focus()

useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

尽可能使用标准的 useEffect 以避免阻塞视觉更新。

useDebugValue

什么时候用React Hook(When)

如果你在编写函数组件并意识到需要向其添加一些state,或者使用一些副作用(可能是生命周期),以前的做法是转化为class,现在可以使用Hook。