Skip to content

Latest commit

 

History

History
423 lines (334 loc) · 11.5 KB

03.Hook.md

File metadata and controls

423 lines (334 loc) · 11.5 KB

Hook

Hook 是 React 16.8.0 的新增特性。

Hook 使你在非 class 的情况下可以使用更多的 React 特性。Hook 不能在 class 组件中使用。

使用规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。

State Hook

useState

使用 useState 可以不通过 class 组件而在函数组件内使用 state,可通过多次调用声明多个 state

  • 参数:

    useState() 方法里面唯一的参数就是初始 state。

  • 返回值:

    当前 state 以及更新 state 的函数。

函数式更新:

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。

function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount)
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
      <button onClick={() => setCount((prevCount) => prevCount - 1)}>-</button>
    </>
  )
}

Effect Hook

Effect Hook 可以让你在函数组件中执行副作用操作(在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。)

useEffect

可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

useEffect 会在每次渲染后(第一次渲染之后和每次更新之后)都执行,如果你的 effect 返回一个函数,React 将会在组件卸载的时候执行清除操作时调用它。

useEffect 在组件内可多次调用,Hook 允许我们按照代码的用途分离他们,React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

使用位置:

组件内部调用 useEffect。 将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。

性能优化:

useEffect 的第二个可选参数可以实现如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用。请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量

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

// 仅在组件初次渲染和组件销毁时执行
useEffect(() => {
  console.log("1")
  return () => {
    console.log("2")
  }
}, [])

示例代码详解 useStateuseEffect

// 引入 React 中的 useState Hook。它让我们在函数组件中存储内部 state
// 引入 useEffect
import React, { useState, useEffect } from "react"

function Example(props) {
  // 声明了一个叫 count 的 state 变量,然后把它设为 0
  const [count, setCount] = useState(0)
  // 声明第2个state
  const [isOnline, setIsOnline] = useState(null)

  // 无需清除的 effect
  useEffect(() => {
    // 将 document 的 title 设置为包含了点击次数的消息。
    document.title = `You clicked ${count} times`
  })

  // 需要清除的 effect
  useEffect(() => {
    function handleFn(val) {
      setIsOnline(val)
    }
    // 注册监听
    XXAPI.subscribe(handleFn)
    // 清除监听
    return () => {
      XXAPI.unsubscribe(handleFn)
    }
  })

  return (
    <div>
      // 读取 State: 我们可以直接用 count
      <p>You clicked {count} times</p>
      // 更新 State: 可以通过调用 setCount 来更新当前的 count
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

useLayoutEffect

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

与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),这时需要用到 useLayoutEffect

useRef

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

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 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。例如:

function Timer() {
  const intervalRef = useRef()

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      // ...
    })
    return () => {
      clearInterval(intervalRef.current)
    }
  })
  // ...
}

其它 Hook

useReducer

useReduceruseState的替代方案,它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

使用示例:

const initialState = { count: 0 }

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 }
    case "decrement":
      return { count: state.count - 1 }
    default:
      throw new Error()
  }
}

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

通过阅读 React 源码 ReactFiberHooks.js 发现 useState 就是对 useReducer 的封装:

// useState
export function useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>] {
  return useReducer(
    basicStateReducer,
    // useReducer has a special case to support lazy useState initializers
    (initialState: any)
  )
}

// useReducer
export function useReducer<S, A>(
  reducer: (S, A) => S,
  initialState: S,
  initialAction: A | void | null
): [S, Dispatch<A>] {
  // ...
  // ...
  // ...
}

利用 useReducer 实现 forceUpdate:

const [ignored, forceUpdate] = useReducer((x) => x + 1, 0)

function handleClick() {
  forceUpdate()
}
useMemo 与 useCallback

可用于给子组件传递参数及回调函数时的优化项

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

function fnComponent() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState("name")

  // 不会在每次fnComponent有更新都重新渲染Child
  const config = useMemo(() => {
    text: `count is ${count}`
  }, [count])
  const handleButtonClk = useCallback(() => {
    setCount((c) => c + 1)
  }, [])

  return (
    <div>
      <div className="countStyle">{name}</div>
      <div className="countStyle">{count}</div>
      <input
        value={name}
        onChange={(e) => {
          setName(e.target.value)
        }}
      />
      <Child config={config} handleButtonClk={handleButtonClk} />
    </div>
  )
}

function Child({ config, handleButtonClk }) {
  console.log("child render")
  return <button onClick={handleButtonClk}>{config.text}</button>
}
useImperativeHandle

解决父组件调用子组件函数的问题,在使用 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={inputRef} /> 的父组件可以调用 inputRef.current.focus()

自定义 Hook

自定义 Hook 是一个函数,其名称以 use 开头(必须以 use 开头),函数内部可以调用其他的 Hook。自定义 Hook 用于提取多组件之间的共享逻辑,可用于替代 render propsHOC

在需要共享逻辑的组件内调用很简单,只需要引入定义好的自定义 Hook,并传入自己想要的参数拿到你想要的返回值作用于当前组件。

如下例:
  1. 提取自定义 Hook:
import React, { useState, useEffect } from "react"

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null)

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline)
    }

    XXXAPI.subscribeToFriendStatus(friendID, handleStatusChange)
    return () => {
      XXXAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange)
    }
  })

  return isOnline
}
  1. 使用自定义 Hook:
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id)

  return <li style={{ color: isOnline ? "green" : "black" }}>{props.friend.name}</li>
}

自定义 hook 实际应用:

  • 获取上一轮的 props 或 state
function Counter() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)
  return (
    <h1>
      Now: {count}, before: {prevCount}
    </h1>
  )
}

function usePrevious(value) {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}
  • 封装axios
import React, { useEffect, useState } from "react";
import Axios from "axios";

export const useFetch = (url, type, config) => {
  const [refresh, setRefresh] = useState(null);
  const [data, setData] = useState(null);
  const [fetching, setFetching] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    const refresh = async () => {
      if (!url) {
        return;
      }
      if (isMounted) {
        setFetching(true);
      }
      const fetchFunction = type === "post" ? Axios.post : Axios.get;
      try {
        const result = await fetchFunction(url, config);
        // If fetching succeeds.
        if (result.status === 200 || result.status === 202) {
          if (isMounted) {
            setData(result.data);
          }
        } else {
          // If fetching fails
          if (isMounted) {
            setError(new Error(result.statusText));
          }
        }
      } catch (e) {
        if (isMounted) {
          setError(e);
        }
      } finally {
        if (isMounted) {
          setFetching(false);
        }
      }
    };
    refresh();
    setRefresh(() => refresh);
    return () => {
      isMounted = false;
    };
  }, [url, type, config]);

  return {
    url,
    type,
    config,
    refresh,
    data,
    fetching,
    error,
  };
};