让你在不编写 class 的状况下使用 state 以及其余的 React 特性【React hook】

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

那么,什么是 Hook?

Hook 是一些可让你在函数组件里“钩入React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。(不推荐把你已有的组件所有重写,可是你能够在新组件里开始使用 Hook。前端

何时使用 Hook?

若是你在编写函数组件并意识到须要向其添加一些 state,之前的作法是必须将其它转化为 class。如今你能够在现有的函数组件中使用 Hook。java

React 内置了一些像 useState 这样的 Hook。你也能够建立你本身的 Hook 来复用不一样组件之间的状态逻辑。先来看看有哪些内置的 Hook。react

个人网站:www.dengzhanyong.com
我的公众号: 【前端筱园

State Hook

这个例子用来显示一个计数器。当你点击按钮,计数器的值就会增长:数组

import React, { useState } from 'react';
​
function Example() {
  // 声明一个叫 “count” 的 state 变量。
  const [count, setCount] = useState(0);
  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
  );
}
复制代码

这里的useState 就是一个hook,在函数组件内容经过调用useState 来添加一些所需的state,它只须要传入一个参数,这个参数就是state的默认初始值,返回一对值:当前状态以及更改变量的方法,这里采用的是数组结构的写法。浏览器

const [count, setCount] = useState(0);
复制代码

它等同于下面的Class组件:性能优化

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
​
  render() {
    return (
      <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div>
    );
  }
}
复制代码

这二者所实现的功能是同样的,能够很明显的看到,使用hook实现的代码量少了不少,不管是声明、使用仍是修改都变得更加的简单。网络

声明 State 变量

在 class 中,咱们经过在构造函数中设置 this.state 为 { count: 0 } 来初始化 count state 为 0:闭包

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
复制代码

在函数组件中,咱们没有 this,因此咱们不能分配或读取 this.state。咱们直接在组件中调用 useState Hook:ide

import React, { useState } from 'react';
​
function Example() {
  // 声明一个叫 “count” 的 state 变量
  const [count, setCount] = useState(0);
}
复制代码

读取 State 变量

当咱们想在 class 中显示当前的 count,咱们读取 this.state.count:

<p>You clicked {this.state.count} times</p>
复制代码

在函数中,咱们能够直接用 count:

<p>You clicked {count} times</p>
复制代码

更新 State 变量

在 class 中,咱们须要调用 this.setState() 来更新 count 值:

<button onClick={() => this.setState({ count: this.state.count + 1 })}>
    Click me
  </button>
复制代码

在函数中,咱们已经有了 setCount 和 count 变量,因此咱们不须要 this:

<button onClick={() => setCount(count + 1)}>
    Click me
  </button>
复制代码

使用多个 state 变量

将 state 变量声明为一对 [something, setSomething] 也很方便,由于若是咱们想使用多个 state 变量,它容许咱们给不一样的 state 变量取不一样的名称:

function ExampleWithManyStates() {
  // 声明多个 state 变量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
复制代码

Effect Hook

Effect Hook 可让你在函数组件中执行反作用操做,仍是上面的例子,如今增长一个功能,当计数器数值改变时,使 document 的 title 也随之改变,你能够像下面这样作:

import React, { useState, useEffect } from 'react';
​
function Example() {
  const [count, setCount] = useState(0);
  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });
​
  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
  );
}
复制代码

若是你熟悉 React class 的生命周期函数,你能够把 useEffect Hook 看作 componentDidMount,componentDidUpdatecomponentWillUnmount 这三个函数的组合。

这时就有人会问了,在Class明明是3个生命周期,在函数中怎么能够只须要一个useEffect函数就能够彻底代替他们?

无需清除的 effect

有时候,咱们只想在 React 更新 DOM 以后运行一些额外的代码。好比发送网络请求,手动变动 DOM,记录日志,这些都是常见的无需清除的操做。由于咱们在执行完这些操做以后,就能够忽略他们了。

在 React 的 class 组件中,render 函数是不该该有任何反作用的。通常来讲,在这里执行操做太早了,咱们基本上都但愿在 React 更新 DOM 以后才执行咱们的操做。

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
​
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
​
  render() {
    return (
      <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div>
    );
  }
}
复制代码

上面是使用class实现的方法,咱们须要在componentDidMountcomponentDidUpdate中都加上更新的操做,这时由于咱们但愿在组件加载和更新的状况下都要进行一样的操做。

回到hook中,只须要在useEffect中写一遍操做便可:

useEffect(() => {
    document.title = `You clicked ${count} times`;
});
复制代码

useEffect 作了什么? 经过使用这个 Hook,你能够告诉 React 组件须要在渲染后执行哪些操做。React 会保存你传递的函数(咱们将它称之为 “effect”),而且在执行 DOM 更新以后调用它。在这个 effect 中,咱们设置了 document 的 title 属性,不过咱们也能够执行数据获取或调用其余命令式的 API。

为何在组件内部调用 useEffect? 将 useEffect 放在组件内部让咱们能够在 effect 中直接访问 count state 变量(或其余 props)。咱们不须要特殊的 API 来读取它 —— 它已经保存在函数做用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的状况下,还引入特定的 React API

useEffect 会在每次渲染后都执行吗? 是的,默认状况下,它在第一次渲染以后和每次更新以后都会执行。你可能会更容易接受 effect 发生在“渲染以后”这种概念,不用再去考虑“挂载”仍是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

须要清除的 effect

以前,咱们研究了如何使用不须要清除的反作用,还有一些反作用是须要清除的。例如订阅外部数据源、settimeout、setinterval等。这种状况下,清除工做是很是重要的,能够防止引发内存泄露!如今让咱们来比较一下如何用 Class 和 Hook 来实现。

在 React class 中,你一般会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。例如,假设咱们有一个 ChatAPI 模块,它容许咱们订阅好友的在线状态。如下是咱们如何使用 class 订阅和显示该状态:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }
​
  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
​
  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}
componentDidMount()与componentWillUnmount()中的内容是相对应的,在class中不得部迫使咱们拆分这些逻辑代码。 复制代码

因为添加和删除订阅的代码的紧密性,因此 useEffect 的设计是在同一个地方执行。若是你的 effect 返回一个函数,React 将会在执行清除操做时调用它:

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);
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
​
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
复制代码

若是对useEffect设置了返回值,那个这个返回值中的内容就至关因而class组件内的componentWillUnmount操做,React会在组件卸载时执行清除操做。

effect像你可使用多个statehook同样,你也可使用多个effect,将不相关逻辑分离到不一样的 effect 中:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
​
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
​
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
}
复制代码

Hook 容许咱们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每个 effect

Effect 进行性能优化

在某些状况下,每次渲染后都执行清理或者执行 effect 可能会致使性能问题。在 class 组件中,咱们能够经过在 componentDidUpdate 中添加对 prevPropsprevState 的比较逻辑解决:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}
复制代码

这是很常见的需求,因此它被内置到了 useEffect 的 Hook API 中。若是某些特定值在两次重渲染之间没有发生变化,你能够通知 React 跳过对 effect 的调用,只要传递数组做为 useEffect 的第二个可选参数便可:

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

若是想执行只运行一次的 effect(仅在组件挂载和卸载时执行),能够传递一个空数组([])做为第二个参数。这就告诉 React 你的 effect 不依赖于 propsstate 中的任何值,因此它永远都不须要重复执行。这并不属于特殊状况 —— 它依然遵循依赖数组的工做方式。

Hook规则

不要在循环,条件或嵌套函数中调用 Hook, 确保老是在你的 React 函数的最顶层调用他们。遵照这条规则,你就能确保 Hook 在每一次渲染中都按照一样的顺序被调用。这让 React 可以在屡次的 useStateuseEffect 调用之间保持 hook 状态的正确

不要在普通的 JavaScript 函数中调用 Hook。你能够在 React 的函数组件中调用 Hook,在自定义 Hook 中调用其余 Hook。

自定义Hook

经过自定义 Hook,能够将组件逻辑提取到可重用的函数中。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部能够调用其余的 Hook。 例如,下面的 useFriendStatus 是就是一个自定义的 Hook:

import { useState, useEffect } from 'react';
​
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
​
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
​
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });
​
  return isOnline;
}
复制代码

与 React 组件不一样的是,自定义 Hook 不须要具备特殊的标识。咱们能够自由的决定它的参数是什么,以及它应该返回什么(若是须要的话)。换句话说,它就像一个正常的函数。可是它的名字应该始终以 use 开头,这样能够一眼看出其符合 Hook 的规则。

此处 useFriendStatus 的 Hook 目的是订阅某个好友的在线状态。这就是咱们须要将 friendID 做为参数,并返回这位好友的在线状态的缘由。

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
​
  // ...return isOnline;
}
复制代码

使用自定义 Hook

如今咱们已经把这个逻辑提取到 useFriendStatus 的自定义 Hook 中,而后就可使用它了:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);
​
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
复制代码
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);
​
  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li>
  );
}
复制代码

自定义 Hook 必须以 “use” 开头吗? 必须如此。这个约定很是重要。不遵循的话,因为没法判断某个函数是否包含对其内部 Hook 的调用,React 将没法自动检查你的 Hook 是否违反了 Hook 的规则。

在两个组件中使用相同的 Hook 会共享 state 吗? 不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),因此每次使用自定义 Hook 时,其中的全部 state 和反作用都是彻底隔离的。

自定义 Hook 如何获取独立的 state? 每次调用 Hook,它都会获取独立的 state。因为咱们直接调用了 useFriendStatus,从 React 的角度来看,咱们的组件只是调用了 useStateuseEffect。咱们能够在一个组件中屡次调用 useState 和 useEffect,它们是彻底独立的。

其余hook

useContext

const value = useContext(MyContext);
复制代码

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

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);
复制代码

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

useCallback

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

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

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
复制代码

把“建立”函数和依赖项数组做为参数传入 useMemo,它仅会在某个依赖项改变时才从新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

useRef

const refContainer = useRef(initialValue);
复制代码

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

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])
复制代码

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

useLayoutEffect

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

useDebugValue

useDebugValue可用于在 React 开发者工具中显示自定义 hook 的标签。

个人网站:www.dengzhanyong.com

相关文章
相关标签/搜索