Loading...

# React Hooks

# Hooks 简介

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

# 基础 Hook

# useState

const [state, setState] = useState(initialState);

返回一个 state,以及更新 state 的函数。

在初始渲染期间,返回的状态 ( state ) 与传入的第一个参数 ( initialState ) 值相同。

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

setState(newState);
//or
setState((value)=>{
    ...
    return newState;
})

在后续的重新渲染中, useState 返回的第一个值将始终是更新后最新的 state。

# 函数式更新

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState 。该函数将接收先前的 state,并返回一个更新后的值。下面的计数器组件示例展示了 setState 的两种用法:

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>
    </>
  );
}

“+” 和 “-” 按钮采用函数式形式,因为被更新的 state 需要基于之前的 state。但是 “重置” 按钮则采用普通形式,因为它总是把 count 设置回初始值。

如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。

# 惰性初始 state

initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

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

# useEffect

useEffect(didUpdate);

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

首先说一下什么是副作用,一个纯函数应该只存在输入到输出的映射,如果有其他的作用,便可称之为副作用

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志、ajax 操作、 等等都是副作用 **(异步操作、延迟操作都算)**,以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。

默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。

# 清除 effect

通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点, useEffect 函数需返回一个清除函数。以下就是一个创建订阅的例子:

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除订阅
    subscription.unsubscribe();
  };
});

为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。在上述示例中,意味着组件的每一次更新都会创建新的订阅。若想避免每次更新都触发 effect 的执行,请参阅下一小节。

# effect 的执行时机

componentDidMountcomponentDidUpdate 不同的是,传给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。

然而,并非所有 effect 都可以被延迟执行。例如,一个对用户可见的 DOM 变更就必须在浏览器执行下一次绘制前被同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。

虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。在开始新的更新前,React 总会先清除上一轮渲染的 effect。

# effect 的条件执行

默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。

然而,在某些场景下这么做可能会矫枉过正。比如,在上一章节的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在 source prop 改变时重新创建。

要实现这一点,可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:

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

此时,只有当 props.source 改变后才会重新创建订阅。

注意

如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。请参阅文档,了解更多关于如何处理函数 以及数组频繁变化时的措施 的内容。

** 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组( [] )作为第二个参数。** 这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循输入数组的工作方式。

如果你传入了一个空数组( [] ),effect 内部的 props 和 state 就会一直持有其初始值。尽管传入 [] 作为第二个参数有点类似于 componentDidMountcomponentWillUnmount 的思维模式,但我们有 更好的 方式 来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect ,因此会使得处理额外操作很方便。

依赖项数组不会作为参数传给 effect 函数。虽然从概念上来说它表现为:所有 effect 函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。

# useContext

const value = useContext(MyContext);

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

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo shouldComponentUpdate ,也会在组件本身使用 useContext 时重新渲染。

useContext 的参数必须是 context 对象本身

  • 正确: useContext(MyContext)
  • 错误: useContext(MyContext.Consumer)
  • 错误: useContext(MyContext.Provider)

调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化

把如下代码与 Context.Provider 放在一起

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>  );
}

# 额外的 Hook

以下介绍的 Hook,有些是上一节中基础 Hook 的变体,有些则仅在特殊情况下会用到。不用特意预先学习它们。

# useReducer

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

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

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

以下是用 reducer 重写 useState 一节的计数器示例:

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: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

注意

React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffectuseCallback 的依赖列表中省略 dispatch

# 指定初始 state

有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer 是最简单的方法:

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

注意

React 不使用 state = initialState 这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,因此需要在调用 Hook 时指定。如果你特别喜欢上述的参数约定,可以通过调用 useReducer(reducer, undefined, reducer) 来模拟 Redux 的行为,但我们不鼓励你这么做。

# 惰性初始化

你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:

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>
    </>
  );
}

# 跳过 dispatch

如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)

需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的 “深层” 节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

# 通过 useContext 和 useReducer 实现的一种状态管理

考虑如下一种情况,有一个书架组件和阅读组件,书架组件可以上传本地书籍,阅读组件可以阅读上传的本地书籍。在书籍没有上传到后台的情况下,如何实现两组件间的通信 (书籍信息共享) 呢?

首先先考虑数据挂载在哪?很明显应该挂在两组件的父组件上 (不考虑 redux 时)。那么既然如此,通过 props 的逐层传递是一种解决方案,但是比较麻烦。更为简便的方案就是通过 useContext 和 useReducer 来实现。

我们可以同通过在其父组件上挂载一个 Context

export const BookListContext = React.createContext();

const reducer = (state, action) => {
  switch (action.type) {
    case "addBooks":
      return handleAddBooks(state, action.value);
    case "deleteBooks":
      return handleDeleteBooks(state, action.value);
    default:
      return state;
  }
};
<BookListContext.Provider
        value={{
          bookList: bookList,
          bookListDispatch: dispatch,
        }}
      >
        <Routes>
          <Route path="/book/:bookName" element={<Book />}></Route>
          <Route path="/" element={<Bookshelf />}></Route>
        </Routes>
</BookListContext.Provider>

子组件需要时,进行调用

  const bookListContext = useContext(BookListContext);
  const bookList=bookListContext.bookList;
  bookListContext.bookListDispatch({type:'addBooks',value:fileLists});
  bookListContext.bookListDispatch({type:'deleteBooks',value:bookName});

# useRef

const refContainer = useRef(initialValue);

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 就像是可以在其 .current 属性中保存一个可变值的 “盒子”。

你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

然而, useRef()ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是, useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时, useRef不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

jluyeyu 微信支付

微信支付

jluyeyu 支付宝

支付宝