6.React Hook 概述,开发中遇到的问题与解决

hooks 函数是做什么用的? 让静态组件动态化

React Hook 概述

什么是 Hook:

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

这篇文章有什么:

这里不过多阐述使用 Hook 的动机,网上都有,如果一定要用 Hook ,这片文章将收集,初次使用 Hook ,所需要知道的干货。

Hook 知识点:

useState √ | useEffect √ | useContext | useReducer | useCallback √ | useMemo √ | useRef √ | useHistory √

(打√的为我用过的,也是比较常用的,其它有机会再用)

useState √

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

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

  • useState:主要是为了给函数添加状态的。最好(必须)在函数的最外层申明使用。
  • initialState:是 state 的初始值,不限制数据类型,不写默认 undefined。
  • setState:是改变 state 的方法,类似于原来的 setState({state:false }),区别是没有钩子函数,也就是不能这样 --> setState({ state:false }, () => { console.log(state) }) 操作。 setState 函数用于更新 state,它接收一个新的 state 值并将组件的一次重新渲染加入队列。

useEffect √

useEffect的作用是依赖变化的时候,执行函数(第一个参数),其中第二个参数为依赖。

第二个参数的作用:

虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。

不写“依赖”,可能会导致没必要的刷新,甚至无限刷新。

写上第二个参数,effect 会监测“依赖”是否变化,当“依赖”变化时才会刷新。

若依赖是一个空数组,effect 会判定依赖没有更新,所以只会执行一次。

effect 第一次被执行的生命周期:第一次 render(创建 DOM),执行 effect ,第二次 render,...

useEffect(() => {
  // do something
  return () => {
    // Clean up side effects
  };
}, []);

的第一个参数,可以返回一个函数,这个函数在组件卸载时执行一次用来清理副效应(订阅、定时器等),

实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。

let timer = null;
function App() {
  const [width,setWidth] = useState(window.innerWidth);
  const [renderFatherState, setRenderFatherState] = useState(0);

  useEffect(()=>{
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize',handleResize);
    timer = setInterval(() => {
      setRenderFatherState(renderFatherState + 1);
    }, 1000)
    return ()=>{ // 清除订阅、定时器等副作用(这里有个错误:不相关 的副作用,不应该写在一起,正确写法看下文)
      window.removeEventListener('resize',handleResize);
      clearInterval(timer);
    }
  }, []);

  return (
    <div>
      <p>{width}</p>
      <p>{renderFatherState}</p>
    </div>
  )
};

注意:如果需要清除多个 不相关 的副作用,不应该写在一起。正确的写法是将它们分开写成两个useEffect()。

所以上述代码正确的写法:

let timer = null;
function App() {
  const [width,setWidth] = useState(window.innerWidth);
  const [renderFatherState, setRenderFatherState] = useState(0);

  useEffect(()=>{
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize',handleResize);
    return ()=>{
      window.removeEventListener('resize',handleResize);
    }
  }, []);

  useEffect(()=>{
    timer = setInterval(() => {
      setRenderFatherState(renderFatherState + 1);
    }, 1000)
    return ()=>{
      clearInterval(timer);
    }
  }, []);

  return (
    <div>
      <p>{width}</p>
      <p>{renderFatherState}</p>
    </div>
  )
};

useContext

之前我们使用context (上下文)来解决多层嵌套传props,分三步

  1. createContext创建Context
  2. 使用Context.Provider组件提供数据
  3. Context.Provider的所有后代组件,都可以通过Context.Consumer使用数据数据

    例子一(react context 的使用):详细举例

    例子二(react hook,useContext 的使用):

      如果需要在组件之间共享状态(比如两个有共同父级的子组件之间),可以使用 useContext()。

      一层的数据传递,感觉没有必用 useContext,如果是多层嵌套的话,父传子的方式就显得很不好了,此时应该用到它。

    下面的例子:Father 中引用 Son,Son 中引用 Grandson,跳过 Son 组件在 Grandson 中直接使用 Father 中的上下文。

    提供状态的父组件:

import React, { createContext} from "react";
import Son from "./components/levelOne";

// 在函数外部传递当前上下文,导出
export const TestContext = createContext("默认值");

const Father = (props) => {

  // 不要在函数内部使用 createContext()
  // const FatherContext = createContext() 

  return (
    <div>
      <span>父级</span>
      <TestContext.Provider value={{ b: 2 }}>
        <Son/>
      </TestContext.Provider>
    </div >
  )
};

export default Father;

需要获取状态的子组件:

import React, { useContext } from "react";
import styles from "./index.less";

// 重点: 从父组件那里引入我们一开始导出的 TestContext 
// 就是父组件的这条代码: export const TestContext = createContext()
import { TestContext } from "../../index";

const Grandson = (props) => {
  // 通过上下文获取到的父组件传递过来的数据.
  // 也可以传递事件,子组件可以通过传递过来的数据触发父组件的事件
  const obj = useContext(TestContext);
  console.log(TestContext, obj);

  return (
    <div>
      <span>第二层子组件</span>
      <br />
      <span>从父级拿到的数据{obj.b}</span>
    </div>
  )
};

export default Grandson;

useReducer

让我们来回忆一下 使用redux使用reducer

// 1.首先创建一个store index.store.js
export default function configStore(){
    const store = createStore(rootReducer,applyMiddleware(...middlewares))
    return store
}

// 2.引入store app.js
  render() {
    return (
      <Provider store={store}>
        <Index />
      </Provider>
    )
  }

// 3.定义action和创建reducder index.action.js index.reducer.js
export const ADD = 'ADD'
export const DELETE = 'DELETE'
function todos(state = INITAL_STATE, action) {
  switch action.type{
    case ADD:{...}
    case DELETE:{...}
  }
}

// 4.页面中使用reducer  component.js
export default connect(mapStateToProps, mapDispatchToProps)(Component);

太复杂了有没有,(使用dva可以简化写法)

而使用useReducer可以省略很多代码:

// index.js
  const { state, dispatch } = useContext(reducerContext);
  return (
    <div className="App">
      <>
        Count: {state.count}
        <button
          onClick={() => dispatch({ type: "reset", payload: { count: 0 } })}
        >Reset</button>
        <button onClick={() => dispatch({ type: "increment" })}>+</button>
        <button onClick={() => dispatch({ type: "decrement" })}>-</button>
        <reducerContext.Provider value={{ state, dispatch }}>
          <ChangeCount />
        </reducerContext.Provider>
      </>
    </div>
  );

不过 useReducer 不支持共享数据,推荐使用 redux-react-hook ,同样是通过 context 实现的 redux

但是不知是否有像查看 store 的浏览器插件,或者 redux-logger 这样的中间件帮助我们查看状态的变化,redux 的生态还是更好一点的

useCallback √

useCallBack

返回:一个缓存的回调函数。

参数:需要缓存的函数,依赖项。

使用场景:父组件更新时,通过props传递给子组件的函数也会重新创建,然后这个时候使用 useCallBack 就可以缓存函数不使它重新创建

使用及场景举例:

// 父组件
import React, { useState, useCallback } from 'react';
import styles from './index.less';
import Child from './Child.jsx';

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

  // 不缓存,每次 count 更新时都会重新创建
  const addFn = () => {
    setCount(count + 1);
  };

  // 使用 useCallBack 缓存
  const handleCountAddByCallBack = useCallback(() => {
    setCount((count) => count + 1);
  }, []);

  return (
    <div className={styles.Test_box}>
      <p>父组件</p>
      <Child add={addFn} addByCallBack={handleCountAddByCallBack}></Child>
    </div>
  );
}

export default Father;

// 子组件
import React, { useState } from 'react';
import styles from './Child.less';
import { Button } from 'antd';

function Child(props) {
  const { add, addByCallBack } = props;

  // 没有缓存,由于每次都创建,memo 认为两次地址都不同,属于不同的函数,所以会触发 useEffect
  useEffect(() => {
    console.log("Child----addFnUpdate");
  }, [add]);

  // 有缓存,memo 判定两次地址都相同,所以不触发 useEffect
  useEffect(() => {
    console.log("Child----addByCallBackFnUpdate");
  }, [addByCallBack]);

  return (
    <div className={styles.Child_box}>
      <Button onClick={() => add()}>点击 + 1</Button>
      <Button onClick={() => addByCallBack()}>点击 + 1</Button>
    </div>
  );
}

export default Child;

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

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

注意:

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

useMemo √

useMemo 是什么呢,它跟 memo 有关系吗, memo 的具体内容可以查看 React 中性能优化、 memo、PureComponent、shouldComponentUpdate 的使用,说白了 memo 就是函数组件的 PureComponent,用来做性能优化的手段,useMemo 也是,useMemo 在我的印象中和 Vue 的 computed 计算属性类似,都是根据依赖的值计算出结果,当依赖的值未发生改变的时候,不触发状态改变。

PureComponent,

它其实就是在帮我们做这样一件事:自动的帮我们编写 shouldComponentUpdate 方法, 避免我们为每个组件都编写一次的麻烦。我们只需要这样, 就可以一步到位

import React, { PureComponent } from 'react'

// 使用 PureComponent ,子组件 不再需要重复编写 shouldComponentUpdate 生命周期,来减少不必要的渲染。
class child extends PureComponent {
  // ...
}

memo

memo 可以提高性能,React.memo 认定两次地址是相同就可以避免子组件冗余的更新

memo 和 useMemo 具体如何使用呢,看下面例子:

import React, { memo, useMemo } from "react";
function App() {
  const [count, setCount] = useState(0);
  const add = useMemo(() => {
    return count + 1
  }, [count]);
  return (
    <div>
      点击次数: { count }
      <br />
      次数加一: { add }
      <button onClick={() => { setCount(count + 1) }}>点我</button>
    </div>
  )
};
export default memo(App) // 导出的时候包一下

useMemo

返回:一个缓存的值

参数:需要缓存的值(也可以是个计算然后再返回值的函数) ,依赖项。

使用场景:组件更新时,一些计算量很大的值也有可能被重新计算,这个时候就可以使用 useMemo 直接使用上一次缓存的值

使用及场景举例:

let timer = null;
function App() {
  const [renderFatherState, setRenderFatherState] = useState(0);
  const [count, setCount] = useState(0);

  useEffect(()=>{
    timer = setInterval(() => {
      setRenderFatherState(renderFatherState + 1);
    }, 1000);
    return ()=>{
      clearInterval(timer);
    };
  }, []);

  const add = useMemo(() => { // useMemo 返回一个缓存的值,通过依赖项来更新缓存。
    console.log("只会根据 count 状态,来触发函数")
    return count + 1;
  }, [count]);
  
  const addM = () => {
    console.log("不相干的状态的改变,比如:renderFatherState,都会触发 addM函数,然而这是不必要得消耗")
    return count + 1;
  });

  return (
    <div>
      <p>{add}</p>
      <p>{addM()}</p>
    </div>
  )
};

useRef √

  • useRef 返回一个可变的ref对象-----书写更加简洁
  • 返回的ref对象在组件的整个生命周期内保持不变
  • ref对象的.current属性设置为相应的DOM节点---------和之前的ref一样的
  • 当ref对象内容发生变化时,useRef并不会通知你--------只是一个记录值,不会帮你监听什么
  • 变更.current属性不会引发组件重新渲染--------只是记录值,并不会监听数据改变和重新渲染操作等

    例子:

import React, { useRef, useEffect } from 'react'
function A(){
  const inputR = useRef();

  useEffect(()=>{
      //页面渲染完成的时候执行
      inputR.current.focus()
  },[]);

  render() {
    return (
      <div>
        <input type="text" ref={inputR} />
        定义属性
      </div>
    );
  };
};

useHistory √

react.js-Hooks 路由跳转

useHistory 钩子允许您访问可能用于导航的历史实例。

import { useHistory } from "react-router-dom";

function HomeButton() {
  let history = useHistory();

  function handleClick() {
    history.push("/home");
  }

  return (
    <button type="button" onClick={handleClick}>
      Go home
    </button>
  );
};

开发中遇到的问题与解决

错误代码:

import BtnGroup from "componten/BtnGroup/BtnGroup";

function Father() {
  const [defaultValue] = useState([
    {name: "全部", key: 0, isAll: true, disabled: false},
    {name: "中国人", key: 1, disabled: false},
    {name: "米国人", key: 2, disabled: false}
  ]);
  const click = () => {
    //...
  }
  return (
    <div>
      <BtnGroup>
        defaultValue={[defaultValue[0]]}
        handleClick={click}
      </BtnGroup>
    </div>
  );
}

export default memo(Father);

// 下面是子组件的部分代码,我在子组件中使用 useEffect 监测 props.defaultValue
useEffect(() => {
  console.log("在这个做一些事,设置组件的初始状态")
},[props.defaultValue])

细心的朋友可能发现了,在 Father 组件每次触发 render 的时候,都会触发子组件的 useEffect ,应为组件的 useEffect 监测了 props.defaultValue,

这时,又有人问了,我给子组件传的 defaultValue 是不会改变的,如何触发了 子组件的 useEffect ?

注意:我给子组件传 defaultValue[0] 的时候,在外面有包了一层 [ ] , 结果是--> [defaultValue[0]] ,也就是说,每次触发 Father 的 render,

子组件的 defaultValue 都会拿到一个新地址的 [ ],同理,只要在 function Father() { } 的函数体内定义的 [ ] (对象) 的指针,都在状态改变的时候指向新的作用域,

从而被 子组件的 useEffect 监测到,触发不必要的刷新。

解决:

  1. 不要监测 props.defaultValue,给一个
  2. 在函数 function Father() { } 体外申明一个变量 defaultValue = [{name: "全部", key: 0, isAll: true, disabled: false}]
  3. 在函数 function Father() { } 体内用 useState 申明变量 const [defaultValue] = useState([{name: "全部", key: 0, isAll: true, disabled: false}])