React学习笔记(十)React Hook

Hook是React16.8的新增特性,让我们能在不使用class的情况下使用state和其他的React属性

Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook不能在class中使用。

React提供了一些内置的Hook,也允许我们去自定义Hook,useState就是其中的一个内置Hook

(有关内置Hook API,请移步https://zh-hans.reactjs.org/docs/hooks-reference.html)

useState

import React, { useState } from 'react';

function ExampleWithManyStates() {
  // 声明多个 state 变量!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}

我们在平时使用组件的时候,如果需要添加一些状态,可以在class中通过this.state去初始化state状态,而在function中没有自己的this,所以在之前,我们不得不转换为class去设置state

但现在,我们可以通过useState Hook来分配和读取this.state

实际上,useState会等价于class里面的一些写法,我们看看下面两段代码

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

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

在使用useState Hook的时候,我们首先需要将其import到当前文件中

import React, { useState } from 'react';

然后,我们就可以在函数中使用它

function Example(){
    // 声明一个叫 “count” 的 state 变量
    const [count, setCount] = useState(0);
}

这里的count,为我们要放到this.state中的属性,而setCount是对count值的设置

在我们调用useState的时候,会创建一个“state变量”,而这个变量,与class中this.state中的属性是一样的,在函数执行完毕之后,这些“state变量”不会被回收

useState只接受一个参数,即初始化时候的值

useState可以被调用多次,调用多次就相当于在class中,给this.state赋值的那个对象传入了多个属性

import React, { useState } from 'react';

function Example(){
    const [a, setA] = useState(0);
    const [b,setB] = useState(0);
}
// 等同于
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      a:0,
      b:0
    };
  }
}

而useState的返回值,为当前state和更新state的函数,所以我们采用

const [a, setA] = useState(0);

来对其进行获取

而如果我们使用了useState的话,比起class,在获取state的时候会更加方便
同样要获取state上的count,两种写法如下

// 使用useState
<p>You clicked {count} times</p>
// 使用class
<p>You clicked {this.state.count} times</p>

同样的,在更新的时候也会更为方便

  • 使用useState
<button onClick={() => setCount(count + 1)}>
    Click me
</button>
  • 使用class
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
    Click me
</button>

Effect Hook

在看Effect Hook如何使用和有什么功能之前,我们先说说出现的原因

在之前的class组件中,存在很多操作,需要我们去在组件挂载和更新时做相同的操作,此时我们不得不写两份相同的代码

比如下面官方文档里面的代码

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

在这段代码中,我们不得不在componentDidMount和componentDidUpdate中写入重复的代码,而实际上,这种情况相当常见,但我们却不得不去犯这个错误

而此时,当我们用到useEffect的时候,完全可以摒弃这个错误

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也需要import,使用useEffect,会使得在函数执行完毕后,React依然保存着传入useEffect的函数参数,当DOM发生更新的时候会去调用这个函数,所以在componentDidMount和componentDidUpdate这两个生命周期都会去触发这个函数

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

而就如在class中我们有componentWillUnmount来对应清除componentDidMount的一些订阅,我们在使用useEffect也有对应的清除方法

我们可以先看看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清除这个订阅,而在useEffect,我们通过返回函数来达到清除的目的

示例如下

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

这里useEffect return出去的,就是清除时的操作。React会在组件卸载的时候执行清除操作。而且这个清除操作,在每次更新组件的时候都会被触发一次。

除了上面提到的,使用useEffect可以用来解决我们需要重复写逻辑相同的代码之外的问题,使用useEffect还可以让我们将关注点分离,针对每一个“订阅”使用一个useEffect,这样子可以做到分离相关逻辑

就像下面的class例子,两个逻辑(计数器和订阅)被耦合在同一个声明周期方法里面

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...

而使用了useEffect让我们可以解决这些问题

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

useContext

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}

useReducer

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer);
  // ...

react hook使用规则

只能在React的函数组件里使用Hook

要注意的是,不要在普通函数里面去使用Hook,我们可以

  • 在React的函数组件里使用Hook
  • 在自定义的Hook里使用Hook

只能在函数最外层调用Hook,不要在循环、条件判断和子函数里面调用Hook

这里只能在函数最外层调用Hook,并不是说Hook不能在循环、条件和子函数里面调用,而是如果不在最外层调用Hook的话,可能会引起一些问题

这与React在多次渲染时怎么判断使用的是同一个Hook有关

在上面我们已经提到了,我们可以同时使用多个useState,但有没有发现一个问题,我们使用useState时,使用的是同一个函数,React是怎么一一对应的呢

答案是按调用的顺序,React默认Hook每次渲染的调用顺序都是一样的,像下面这段官网的代码

// ------------
// 首次渲染
// ------------
useState('Mary')           // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm)     // 2. 添加 effect 以保存 form 操作
useState('Poppins')        // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle)     // 4. 添加 effect 以更新标题

// -------------
// 二次渲染
// -------------
useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm)     // 2. 替换保存 form 的 effect
useState('Poppins')        // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle)     // 4. 替换更新标题的 effect

// ...

而我们要保证调用顺序一样,自然就不可以去用到条件语句,包括循环语句,一旦循环条件的判断是受其他因素影响的,在多次渲染时会不一样,那一样会导致顺序的不同

如果我们将代码放到条件语句中,如下

// 🔴 在条件语句中使用 Hook 违反第一条规则
if (name !== '') {
    useEffect(function persistForm() {
        localStorage.setItem('formData', name);
    });
}

如果一开始判断条件为true,后来变成false,就会变成下面这样

useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm)  // 🔴 此 Hook 被忽略!
useState('Poppins')        // 🔴 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle)     // 🔴 3 (之前为 4)。替换更新标题的 effect 失败

React不知道这里第二个useState的Hook应该返回什么。因为它以为还和上次渲染一样,应该对应persistForm的useEffect,因此导致了后面所有的Hook的错误

自定义Hook

通过自定义Hook,我们可以将一些相同的逻辑添加到一个自定义Hook中,而这个自定义Hook实际上就是一个函数

看看下面的代码

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 () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ---------------------------------------------------------------------

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}


function FriendListItem(props) {
  // ---------------------------------------------------------------------
  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);
    };
  });
  // ---------------------------------------------------------------------

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

可以看到,在这两个组件里面,Hook的逻辑是完全相同的,自定义Hook就可以让我们将这部分逻辑“封装”起来

自定义Hook本身其实就是一个函数,我们可以在Hook中使用其他Hook,但要注意的是,我们在使用自定义Hook的时候,里面使用到的Hook,也要放在自定义Hook的最外层

与函数组件不同的是,我们可以决定自定义Hook要传入的参数是什么,要返回什么,也就是说,它是一个正常的函数,但是需要以use开头,我们对上面的代码,通过使用自定义Hook,在不同的组件中共享Hook的逻辑

我们将上面的注释间的内容放到下面这个自定义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;
}

接着,改变上面的两个函数组件

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,并不会共享里面的state,我们可以看下面这两个函数组件,使用了同一个自定义Hook

function useTest(){
    const [count,setCount] = useState(1) 
    return [count,setCount]
}

function Test1(){
    const [count,setCount] = useTest()
    return(
        <div>
        <span>{count}</span>
        <button onClick={()=>{setCount(count+1)}}>btn1</button>
        </div>
    )
}
function Test2(){
    const [count,setCount] = useTest()
    return(
        <div>
        <span>{count}</span>
        <button onClick={()=>{setCount(count+1)}}>btn2</button>
        </div>
    )
}

这里点击btn1的时候,Test2组件里面的count并不会发生改变

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值