Hook API 使用篇

747 阅读6分钟

本篇将重点介绍 Hook API 的具体使用方式。之后的文章会介绍原理。

useState

我们首先使用Hook来声明一个state,这是不管做什么都需要的一个API。

import React, { useState } from 'react';
function Example() {
    // 声明一个叫'count' 的 state
    const [count, setCount] = useState(0)
}

调用 useState 方法的时候都做了什么?

帮我们定义了一个state变量,这个变量叫做count,可以叫做任何名字。一般来说,在函数推出之后变量就会“消失”,而state中的变量会被React保留。

useState 需要哪些参数?

只需要一个参数,这个参数将会是count的默认值。我们可以传数字、字符串、对象等,这完全看你个人的需求。如果你想创建两个变量,再多调用一次useState。

useState 的返回值是什么?

返回的是当前的state,以及修改state的函数。setCount这个函数将只会修改count,所以要特别注意,避免命名冲突。

如何读取/更新state?

我们想在DOM中使用我们的state,展示到页面上:

import React, { useState } from 'react';
function Example() {
    // 声明一个叫'count' 的 state
    const [count, setCount] = useState(0)
    return (
        <div>这是 conunt:{ count }</div>
    )
}

我们想要更新页面的state,从而达到数据响应式:

import React, { useState } from 'react';
function Example() {
    // 声明一个叫'count' 的 state
    const [count, setCount] = useState(0)
    const fn = () => {
        setCount(count + 1)
    }
    return (
        <div>这是 conunt:{ count }</div>
        <button onClick={() => fn()}></button>
    )
}

useEffect

在react组件中有两种常见的副作用操作,需要清除的和不需要清除的。

无需清除的 effect

有时候,我们只想在 React 更新DOM之后运行一些额外的代码。比如发送网络请求,记录日志等,这些都是常见的不需要请求的操作。因为我们在执行完这些操作之后,就可以忽略他们了。

看看下面这段代码:

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {    
      document.title = `You clicked ${count} times`;  
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect 做了什么?

这个Hook可以做到,在渲染结束之后执行哪些操作。

React内部会保存你传递的函数,在DOM更新之后调用这个函数(这个函数称为effect)。

为什么在组件内部调用uesEffect?

放在组件内能够直接访问count变量,或者其他的props。

Hook 使用了 JavaScript 的闭包机制。

useEffect 会在每次渲染之后都执行么?

默认情况下是这样的,React 保证了在每次运行 effect 函数的同时,DOM都是更新完毕的。

他也是可以进行控制的。

注意

  • 每次重新渲染,都会生成新的 effect,替换掉之前的 effect。
  • 与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 不会阻塞浏览器更新屏幕。

需要清除的 effect

比如说订阅了外部数据源,这种情况下清除 effect 是非常重要的,为了防止内存泄漏。

来看下面这个例子:

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {    
      function handleStatusChange(status) {      
          setIsOnline(status.isOnline); 
      }
      ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
      // Specify how to clean up after this effect:
      return function cleanup() {
              ChatAPI.unsubscribeFromFriendStatus(
              props.friend.id, 
              handleStatusChange);
       };  
  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

如果你的effect返回一个函数,React会在执行清除操作时调用这个函数。

为什么要在 effect 中返回一个函数?

设计的想法是:这两部分代码都作用于相同的副作用,它们都属于effect的一个部分,而不是将他们进行拆分。

React 何时清除 effect?

他会在组件卸载的时候执行清除操作。

他是在调用新的effect之前对之前的effect进行清除。比如,第一个effect被调用,而清除的effect将会被存储,等第二次调用effect的时候,先回清除第一个effect。

通过跳过 Effect 进行性能优化

在某些情况下,每次渲染之后都要执行effect,可能会导致性能问题。

如果某些特定的值在两次重新渲染之间没有发生变化,就可以跳过对effect函数的执行。如下面的例子:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

第二个参数就是为了如此,如果[count]中的count没有发生改变,就不会执行effect。

如果只想执行一次effect,就传递一个空数组。

useContext

这个Hook需要配合其他的方法使用,举个例子来看:

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);  
  return (    
      <button style={{ background: theme.background, color: theme.foreground }}>      
          I am styled by theme context!    
      </button>  
  );
}

首先由 React.createContext 返回一个 context 对象。

我们的组件被ThemeContext.Provider包裹,例如:<ThemeContext.Provider value={themes.dark}>我们的组件</ThemeContext.Provider>,这样value会被层层传递下去。哪怕再深的子组件都会接收到value传递的值。

深层的子组件可以通过,useContext(ThemeContext)方式来接收外层传递进来的value,useContext会返回这个value。

优点

  • 不会再使用props进行传递了。
  • 不会出现props那样子逐层传递过深了。
  • 状态管理变得更轻松。哪怕是兄弟组件,可以通过对象的方法进行修改同一个状态。

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

三个参数:

  • reducer: 执行函数。
  • initialArg: 默认值。
  • init: 惰性初始化数据。

你可能不明白什么是惰性初始化数据,让我来看这个例子:

function init(initialCount) {  
    return {count: initialCount}
}
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':      
      return init(action.payload);    
    default:
      throw new Error();
  }
}

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

Counter这个函数和将会接受一个参数,这个参数会通过useReducer传递给init函数,然后会作为state的默认值。

当调用dispatch时,传递一个参数,这个参数作为reducer的action,而init函数的返回值,作为reducer的state。

会不会觉得很绕?看一下这个例子:codesandbox.io/s/relaxed-b…

跳过 dispatch

如果 Reducer Hook 的返回值与当前state相同,React 将跳过子组件的渲染以及副作用的执行。

React 内部使用的是 Object.is比较算法 来比较state的。

useCallback

useCallback的两个参数:

  • 函数,当监听的第二个参数发生改变时被调用

  • 数组,监听数组内的值是否发生变化,如果是空数组将只执行一次。 useCallback的返回值:

  • 返回给我们一个函数。 看下面这个例子

import "./styles.css";

import React, { useCallback, useState } from "react";

export default function App() {
    const [a, setA] = useState(0);
    const [b, setB] = useState(0);
    const memoizedCallback = useCallback(() => {
        console.log(a, b);
        setA(a + 1);
        setB(b + 1);
    }, [a, b]);
    return (
        <div className="App">
            <h1>Hello CodeSandbox</h1>
            <h2>Start editing to see some magic happen!</h2>
            {a}----{b}
            <button onClick={() => memoizedCallback()}>click</button>
        </div>
    )
}

当点击click按钮时,调用useCallback返回的函数memoizedCallback,会对useCallback第一个参数的函数进行调用。

useMemo

useMemo的两个参数:

  • 函数,当依赖项数组发生改变之后调用
  • 依赖项数组,如果没有提供,每次都会渲染时都会计算新的值。

useMemo返回值:

  • 返回的是value值,这与useCallback不同。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

仅仅会在某个依赖项改变的时候进行重新计算。这样会避免每次渲染时都会进行高开销的计算。

useRef

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useRef会返回一个可变的Ref对象,.current 属性被初始化为传入的参数。

当 .current 属性发生改变,并不会引发组件的重新渲染。

useLayoutEffect

函数签名与useEffect相同,淡会在你所有的DOM变更之后同步调用 effect。

可以使用它来读取DOM布局,并同步触发重新渲染。

useLayoutEffect是在浏览器绘制执行之前,内部的更新计划会被同步刷新。

useImperativeHandle

它的作用是让子组件在使用ref的时候,可以控制哪些值不需要暴露给父组件。

他因该与forwardRef一起使用。

看下面这个例子:

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

FancyInput将会成为<FancyInput ref={inputRef}> 组件,而父组件只可以调用focus函数中暴露出去的inputRef.current.focus()方法。

useDebugValue

他是在React开发工具中显示自定义Hook标签。

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  // ...
  // 在开发者工具中的这个 Hook 旁边显示标签  
  // e.g. "FriendStatus: Online"  
  useDebugValue(isOnline ? 'Online' : 'Offline');
  return isOnline;
}

可以接受一个格式化函数作为第二个参数,这个函数只有Hook被检查的时候才会被调用。他接受debug的值作为参数,并且会返回一个格式化的显示值。

useDebugValue(date, date => date.toDateString());

比如上面的这个例子,一个返回date值的自定义Hook可以通过格式化函数来避免不必要的toDtaeString函数调用。

总结

  • 基础 Hook

    • useState
    • useEffect
    • useContext
  • 额外的 Hook

    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue