react hooks

Posted webchang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react hooks相关的知识,希望对你有一定的参考价值。

文章目录


强烈推荐这个react-hooks的学习教程:https://github.com/puxiao/react-hook-tutorial

react hooks

react hooks是什么?

(1). Hook是React 16.8.0版本增加的新特性/新语法
(2). 可以让你在函数组件中使用 state 以及其他的 React 特性

三个常用的Hook
(1). State Hook: React.useState()
(2). Effect Hook: React.useEffect()
(3). Ref Hook: React.useRef()

useState

(1). State Hook让函数组件也可以有state状态, 并进行状态数据的读写操作
(2). 语法: const [xxx, setXxx] = React.useState(initValue);
(3). useState()说明:
参数: 第一次初始化指定的值在内部作缓存
返回值: 包含2个元素的数组,第1个为内部当前状态值,第2个为更新状态值的函数
(4). setXxx()2种写法:

  • setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值。如果多次连续调用,最后只会更新最近一次的值(合并更新一次状态)。
  • setXxx(value => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值。如果多次连续调用,参数value是最新的值,会更新多次状态。
function Demo() 
  // useState函数参数,初始化指定的值会在内部作缓存
  // 如果要使用多个状态,需要编写多次React.useState()
  const [count, setCount] = React.useState(1);
  const [name, setName] = React.useState("小明");

  function add() 
    // setCount(count + 1);
    // setCount的第二种写法
    setCount(count => count + 1);
  

  function changeName() 
    setName("晓明");
  

  return (
    <div>
      <h2>求和为:count</h2>
      <h2>姓名:name</h2>
      <button onClick=add>+1</button>
      <button onClick=changeName>改名字</button>
    </div>
  );

setXxx是采用 “异步直接赋值” 的形式,并不会像类组件中的setState()那样做“异步对比累加赋值”。
“异步”?这里的“异步”和类组件中setState中的异步是同一个意思,都是为了优化React渲染性能而故意为之。也就是无法在setXxx之后立即拿到最新的值。

“直接赋值”:
1、在Hook中,对于简单类型数据,比如number、string类型,可以直接通过setXxx(newValue)直接进行赋值。
2、但对于复杂类型数据,比如array、object类型,若想修改其中某一个属性值而不影响其他属性,则需要先复制出一份,修改某属性后再整体赋值。

通过 setXxx 设置新值,但是如果新值和当前值完全一样,那么会引发React重新渲染吗?通过React官方文档可以知道,当使用 setXxx 赋值时,Hook会使用Object.is()来对比当前值和新值,结果为true则不渲染,结果为false就会重新渲染

useEffect

(1)Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)。
(2)React中的副作用操作

  • 发ajax请求数据获取
  • 设置订阅 / 启动定时器
  • 手动更改真实DOM

(3)语法和说明:useEffect(effect,[deps])函数可以传入2个参数,第1个参数为我们定义的执行函数、第2个参数是依赖关系(可选参数)。若一个函数组件中定义了多个useEffect,那么他们实际执行顺序是按照在代码中定义的先后顺序来执行的。

useEffect(() =>  
  // 在此可以执行任何带副作用操作
  return () =>  // 在组件卸载前执行
    // 在此做一些收尾工作, 比如清除定时器/取消订阅等
  
, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行

(4)可以把 useEffect Hook 看做如下三个函数的组合

  • componentDidMount()
  • componentDidUpdate()
  • componentWillUnmount()

(5)在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。

  • 有时候,我们只想**在 React 更新 DOM 之后运行一些额外的代码。**比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。
  • 一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!

如果React.useEffect只传入了第一个函数参数,相当于是检测所有的状态。当页面第一次加载时会执行一次,每当有状态更新时也会执行该函数,也就是在每次渲染的时候都会执行。相当于是componentDidMount和componentDidUpdate。

function Example() 
  const [count, setCount] = useState(0);

  useEffect(() => 
    document.title = `You clicked $count times`;
  );

有些副作用可能需要清除,这是可以返回一个函数,返回的函数相当于是componentWillUnmount

useEffect(() => 
  function handleStatusChange(status) 
    setIsOnline(status.isOnline);
  
	
  // 订阅
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => 
    // 取消订阅
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  ;
);

可以使用多个Effect Hook,Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 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);
    ;
  );
  // ...

如果传入第二个参数是空数组,则第一个函数参数只会执行一次,谁也不监测。则第一个函数参数相当于是componentDidMount​

如果传入的第二个参数不是空数组,数组中有对应的元素[count],则只检测该状态的改变,状态改变时会重新执行函数。

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

总结:useEffect的第二个参数可分为以下几种情况。
1、若缺省,则组件挂载、组件重新渲染、组件即将被卸载前(return的函数),每一次都会触发该useEffect;
2、若传值,则必须为数组,数组的内容是函数组件中通过useState自定义的变量或者是父组件传值过来的props中的变量。在组件挂载时会执行一次,之后只有数组内的变量发生变化时才会触发useEffect;
3、若传值,但是传的是空数组 [],则表示该useEffect里的内容仅会在“挂载完成后和组件即将被卸载前(return的函数)”执行一次;

常见问题

下方案例中,count的值从0变为1后就不再变化?为什么?

function Demo() 
  const [count, setCount] = React.useState(1);

  // 使用生命周期钩子,这里相当于是componentDidMount和componentWillUnmount的结合
  React.useEffect(() => 
    let timer = setInterval(() => 
      setCount(count + 1);
    , 1000);
    return function () 
      clearInterval(timer);
    ;
  , []);
  
  // 卸载组件
  function unMount() 
    ReactDom.unmountComponentAtNode(document.getElementById("root"));
  

  return (
    <div>
      <h2>count</h2>
      <button onClick=unMount>卸载组件</button>
    </div>
  );

关键点在于我们对useEffect传入来一个空数组,并且使用了setCount(count + 1);

如果传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过 1。

只需要改为setCount(count => count + 1)就可以解决这个问题,这种更新不再依赖于外部的count变量。

官网解释:https://zh-hans.reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often

useRef

(1). Ref Hook可以在函数组件中存储/查找组件内的标签或任意其它数据
(2). 语法: const refContainer = React.useRef()
useRef可以“获取某些组件挂载完成或重新渲染完成后才拥有的某些对象”的引用,且保证该引用在组件整个生命周期内固定不变,都能准确找到我们要找的对象。

useRef关联对象的2种用法:
1、针对 JSX组件(原生标签),通过属性 ref=xxxRef 进行关联。
2、针对 useEffect中的变量,通过 xxxRef.current 进行关联。

//先定义一个xxRef引用变量,用于“勾住”某些组件挂载完成或重新渲染完成后才拥有的某些对象
const xxRef = useRef(null);

//针对 JSX组件,通过属性 ref=xxxRef 进行关联
<xxx ref=xxRef />

//针对 useEffect中的变量,通过 xxxRef.current 进行关联
useEffect(() => 
   xxRef.current = xxxxxx;
,[]);

补充说明:
1、useRef是专门针对函数组件的,如果是类组件则使用React.createRef()。
2、React.createRef()也可以在函数组件中使用。
只不过React.createRef创建的引用不能保证每次重新渲染后引用固定不变。如果你只是使用React.createRef“勾住”JSX组件转换后对应的真实DOM对象是没问题的,但是如果想“勾住”在useEffect中创建的变量,那是做不到的。二者都想可以“勾住”,只能使用useRef。

在线案例:https://codesandbox.io/s/useref-40sxf5

使用案例1

若我们有一个组件,该组件只有一个输入框,我们希望当该组件挂载到网页后,自动获得输入焦点。
需求分析:需要使用useRef “勾住”这个输入框,当它被挂载到网页后,通过操作原生html的方法,将焦点赋予该输入框上。

import React,useEffect,useRef from 'react'

function Component() 
  //先定义一个inputRef引用变量,用于“勾住”挂载网页后的输入框
  const inputRef = useRef(null);

  useEffect(() => 
    //inputRef.current就是挂载到网页后的那个输入框,一个真实DOM,因此可以调用html中的方法focus()
    inputRef.current.focus();
  ,[]);

  return <div>
      /* 通过 ref 属性将 inputRef与该输入框进行“挂钩” */
      <input type='text' ref=inputRef />
    </div>

export default Component

注意:
1、在给组件设置 ref 属性时,只需传入 inputRef,千万不要传入 inputRef.current。
2、在“勾住”渲染后的真实DOM输入框后,能且只能调用原生html中该标签拥有的方法。

使用案例2

若我们有一个组件,该组件的功能需求如下:
1、组件中有一个变量count,当该组件挂载到网页后,count每秒自动 +1。
2、组件中有一个按钮,点击按钮可以停止count自动+1。

import React,useState,useEffect,useRef from 'react'

function Component() 
  const [count,setCount] =  useState(0);
  const timerRef = useRef(null);//先定义一个timerRef引用变量,用于“勾住”useEffect中通过setIntervale创建的计时器

  useEffect(() => 
    //将timerRef.current与setIntervale创建的计时器进行“挂钩”
    timerRef.current = setInterval(() => 
        setCount((prevData) =>  return prevData +1);
    , 1000);
    return () => 
        //通过timerRef.current,清除掉计时器
        clearInterval(timerRef.current);
    
  ,[]);

  const clickHandler = () => 
    //通过timerRef.current,清除掉计时器
    clearInterval(timerRef.current);
  ;

  return (
    <div>
        count
        <button onClick=clickHandler >stop</button>
    </div>
  )


export default Component

在 TS中使用 useRef 创建计时器注意事项

timerRef.current = setInterval(() => 
    setCount((prevData) =>  return prevData +1);
, 1000);

如果是在 TS 语法下,上面的代码会报错误:不能将类型“Timeout”分配给类型“number”。
造成这个错误提示的原因是:

  1. TypeScript 是运行在 Nodejs 环境下的,TS 编译之后的代码是运行在浏览器环境下的。
  2. Nodejs 和浏览器中的 window 他们各自实现了一套自己的 setInterval
  3. 原来代码 timerRef.current = setInterval( … ) 中 setInterval 会被 TS 认为是 Nodejs 定义的 setInterval,而 Nodejs 中 setInterval 返回的类型就是 NodeJS.Timeout。
  4. 所以,我们需要将上述代码修改为:timerRef.current = window.setInterval( … ),明确我们调用的是 window.setInterval,而不是 Nodejs 的 setInterval。

useContext

useContext是来解决什么问题?

useContext是<XxxContext.Consumer>的替代品,可以大量简化获取共享数据值的代码

补充说明:
1、函数组件和类组件,对于<XxxContext.Provider>、<XxxContext.Consumer>使用方式没有任何差别。
2、你可以在函数组件中不使用useContext,继续使用<XxxContext.Consumer>,这都没问题。只不过使用useContext后,可以让获取共享数据相关代码简单一些。

使用案例

const value = useContext(MyContext);

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

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。

useContext(MyContext) 只是让你能够_读取_ context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件_提供_ context。

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

示例中是把XxxContext直接定义在组件内部,没有使用import,但是这种方式没有办法在其它组件中使用XxxContext。如果将XxxContext定义在外部单独的模块中,各个组件都可以引用。

下方示例中,ContextCmp --> MiddleCmp --> ChildCmp,ContextCmp组件要传递数据给ChildCmp

// context.js
import React from "react";
export const PersonContext = React.createContext( name: "a", age: 10 );

// ContextCmp.jsx
import  PersonContext  from "./context";
import MiddleCmp from "./MiddleCmp";

export default function ContextCmp() 
  return (
    <div>
      <h1>ContextCmp</h1>
      <PersonContext.Provider value= name: "b", age: 1 >
        <MiddleCmp />
      </PersonContext.Provider>
    </div>
  );


// MiddleCmp.jsx
import ChildCmp from "./ChildCmp";

export default function MiddleCmp() 
  return (
    <div>
      <h2>MiddleCmp</h2>
      <ChildCmp />
    </div>
  );


// ChildCmp.jsx
import  useContext  from "react";
import  PersonContext  from "./context";

export default function ChildCmp() 
  const data = useContext(PersonContext);
  return (
    <div>
      <h3>ChildCmp</h3>
      <div>
        data.name--data.age
      </div>
    </div>
  );


useContext只是简化了获取共享数据value的代码,但是对于<XxxContext.Provider>的使用没有做任何改变

为什么不使用redux

在Hook出现以前,React主要负责视图层的渲染,并不负责组件数据状态管理,所以才有了第三方Redux模块,专门来负责React的数据管理。

但是自从有了Hook后,使用React Hook 进行函数组件开发,实现数据状态管理变得切实可行。只要根据实际项目需求,使用useContext以及useReducer,一定程度上是可以满足常见需求的。

useReducer

useReducer(reducer,initialValue)函数通常传入2个参数,第1个参数为我们定义的一个“由dispatch引发的数据修改处理函数”,第2个参数为自定义数据的默认值,useReducer函数会返回自定义变量的引用和该自定义变量对应的“dispatch”。

useReducer是useState的升级版,可以实现复杂逻辑修改,而不是像useState那样只是直接赋值修改。补充说明:
1、在React源码中,实际上useState就是由useReducer实现的,所以useReducer准确来说是useState的原始版。
2、无论哪一个Hook函数,本质上都是通过事件驱动来实现视图层更新的。

代码形式:

import React,  useReducer  from 'react'; //引入useReducer

//定义好“事件处理函数” reducer
function reducer(state, action) 
  switch (action) 
    case 'xx':
        return xxxx;
    case 'xx':
        return xxxx;
    default:
        return xxxx;
  


function Component()
  //声明一个变量xxx,以及对应修改xxx的dispatch
  //将事件处理函数reducer和默认值initialValue作为参数传递给useReducer
  const [xxx, dispatch] = useReducer(reducer, initialValue); 

  //若想获取xxx的值,直接使用xxx即可
  
  //若想修改xxx的值,通过dispatch来修改
  dispatch('xx');


//请注意,上述代码中的action只是最基础的字符串形式,事实上action可以是多属性的object,这样可以自定义更多属性和更多参数值
//例如 action 可以是 type:'xx',param:xxx

使用案例

import React,  useReducer, useState  from "react";

function reducer(state, action) 
  const  type, payload  = action;
  switch (type) 
    case "add":
      return state + payload;
    case "sub":
      return state - payload;
    case "mul":
      return state * payload;
    default:
      return state;
  


export default function Count() 
  const [num, setNum] = useState(0);
  const [count, dispatch] = useReducer(reducer, 0);

  return (
    <div>
      <h3>count</h3>
      react hooks之useDebounce useThrottle

React Hooks - 引擎盖下发生了啥?

React Hook的使用

React Hooks --- useState 和 useEffect

react之Hook的useEffect详解

React hooks之useEffect