React 部分hooks的使用及注意

React让我们可以在不使用class component的情况下,使用state以及React其他的一些特性

具体的讲,react能够让我们在function component中管理state处理side effect,同时,可以抽离出公共逻辑到自定义effect中,这是我们使用hooks,经常用到的三个主要功能

hooks解决的问题

  • 在组件之间复用状态逻辑(stateful logic)很困难,虽然可以使用props和高阶组件(HOC),但是这些方案需要我们重新组织组件结构,使得代码更加难以理解,而且容易形成“嵌套地狱”
  • 复杂组件变得难以理解
  • 难以理解的class

复杂组件难以理解

主要是在class 组件中,会包含很多状态逻辑以及副作用,每个生命周期函数中包含一些不想关的逻辑,很多相关的逻辑分散到不同的地方不相关的逻辑又在同一个方法中组合

如:

componentDidMount() {	
	fetchData(); 		// 获取数据
  subscribe();		// 订阅
  addListen();		// 监听
}

componentWillUnmount() {
  unsubscribe(); 	// 取消订阅
  removeListen();	// 取消监听
}
复制代码

在上面代码中,我们常在componentDidMount() 钩子函数中获取数据、订阅和监听,这些本是没什么关联性的逻辑,却被都放在一个方法中,而取消订阅和取消监听 是和订阅和监听相关联的,却本分散到不同的地方,如此很容易产生bug,并且导致逻辑不一致

难以理解的class

class的学习比较困难,JavaScript中this的工作方式比较难掌握,而且,还不能忘记绑定事件处理器(PS:主要是事件处理器中的this指向问题,有三种处理方式:1. 在constructor中绑定this,2. 使用箭头函数定义事件处理函数,3. 在元素上绑定事件处理函数时直接使用箭头函数)

参考:hook简介

hooks规则

  • 只在顶层调用hooks
    • 不在循环、条件语句和嵌套函数内调用hooks
    • 在return之前调用
    • 保证每次渲染时hooks的调用顺序是一致的
  • 只在function component组件中使用hooks

为什么只能在顶层调用hook?

因为可以在一个函数组件中多次调用state和hooks,那么如何知道state是与哪个useState相对应?

答案是:react依赖hooks调用的顺序

如果我们在循环、条件语句或嵌套函数内调用hooks,那么在每次渲染的时候,有可能会因为条件的不同,导致hooks的调用顺序不同,而无法获取到正确的state或运行出错,所以,要在顶层调用hook

具体查看:hooks-rule

一些useEffect

useState

useState hook中使用setState时,是replace state(替换),不同于class component中的setState是merge state(合并)

useEffect

主要用于处理 副作用(side effects) 的hook

什么是副作用(side effects)?

指那些会影响其他组件以及在渲染过程中无法完成的操作,如:获取数据、订阅、操作dom

useEffect hook 提供的功能与class component中的componentDidMount、componentDidUpdate、componentWillUnmount 等钩子函数提供的功能相同,但是统一成了一个api——useEffect

需要注意的是:useEffect的执行时机,它会在组件渲染之后被异步执行

关于useEffect以及useLayoutEffect的执行时机和使用区别

react的执行过程主要包括三层:scheduler(调度器)reconciler(协调器——render阶段)renderer(渲染器——commit阶段)

render阶段: 主要是根据接收到的新的react element来生成新的fiber树的过程

commit阶段:根据fiber树更新dom树,并渲染到页面中,commit主要可以分为三个阶段:before mutation——执行dom操作前mutation——执行dom操作layout——执行dom操作后

before mutation 主要工作:

  • 调用getSnapshotBeforeUpdate生命周期函数(可以理解为componentWillUpdate的替代函数)
  • 调度useEffect?(注意:不是调用

mutation主要工作

根据effectTag的类型执行不同的操作

  • Placement: 插入DOM节点
  • Update: 执行上一次更新useLayoutEffect(create, deps)销毁函数
  • Delete: 调用class component的componentWillUnmount调度function component的useEffect(create, deps)销毁函数(注:是调度, 不是调用)

layout主要工作

  • class component
    • 通过current是否为null来判断组件是mount还是update,如果是mount,则调用componentDidMount,如果是update,则调用componentDidUpdate
    • 调用this.setState(data, callback)的第二参数callback,也就是回调函数
  • function component
    • 调用useLayoutEffect(create, deps)的create回调函数
    • 调度useEffect(create, deps)的create回调函数?(注意:也是调度,不是调用)

⚡️注意:上面描述的各hook的执行或调度时机可能并不准确,需要查看react-dom源码确认,但是有几点是可以确定的:

  • useLayoutEffect是同步执行的,useEffect是异步执行的(effect 的执行时机
  • useEffect(create, deps)是异步执行的,且是在渲染完成后延迟执行(也就是layout执行完成后),至于为什么需要异步执行,简单地说就是为了防止其阻塞浏览器的渲染,具体查看useEffect执行时机,同时,它的销毁函数的执行时机也是在执行下次effect之前被执行,也就是说在执行下次effect之前清除上一次渲染的副作用(也就是它的销毁函数),注意:effect的销毁函数会在组件UNmount时被调度
  • useLayoutEffect(create, deps)的create方法是在commit阶段的layout时期同步执行
  • 如果需要进行DOM操作且**要求同步(**synchronize)执行,可以放到useLayoutEffect(create, deps) hook中执行
  • 在实际开发中,我们可以将需要同步修改DOM的操作放到useLayoutEffect hook中执行,因为在执行该hook时,DOM还未被渲染到浏览器上,还只是存放在内存中(PS:浏览器的渲染线程JavaScript引擎线程互斥的,当执行JavaScript时,渲染线程被阻塞),所以,在该hook中执行DOM操作不会导致浏览器多次渲染(因为当前更新还未被渲染到页面上),但是会阻塞渲染,如果是放到useEffect中执行,会导致多次渲染,因为useEffect是异步执行的,会在当前渲染完成后延迟执行,但是会在下一次更新前执行完,所以会导致多次渲染,也就是会导致浏览器回流重绘,注意这个重要的区别,简而言之,useLayoutEffect是在渲染前或更新前执行,useEffect是在渲染后或更新后被执行,查看效果
  • 在effect添加了依赖项的情况下,当依赖更新,导致重新渲染,会先调用effect的销毁函数,然后再调用create函数,如果没有添加依赖,那么组件每次重新渲染都会先调用销毁函数,然后再调用create函数,查看效果

useContext

在函数组件中使用Context,在什么情况使用?

就像修改主题颜色这种需要将主题颜色一层一层传递到多个组件的情况就比较适合使用context

使用步骤:

  1. 使用React.createContext(initValue)创建一个context,如:const MyContext = React.createContext(initValue)
  2. 使用 <MyContext.Provider> 就值传递下去,如:<MyContext.Provider value={value}><MyComponent /> </MyContext.Provider>
  3. 在需要使用context的组件中引入MyContext,如:const context = React.useContext(MyContext)
  4. 从context中获取传下来的值

具体使用情况查看代码

注意点:

  • <MyContext.Provider>更新时,该 Hook 会触发重渲染,并使用最新传递给<MyContext.Provider>的context value的值,即使祖先组件使用了React.memoshouldComponentUpdate 也会触发使用useContext hook组件的重新渲染
  • 调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化重要

useReducer

某些场景下useReducer可能比useState更加适用,如:

  • state逻辑比较复杂,且下一个state依赖之前的state

另外文档上说:

使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

不是很理解这句话,虽说可以使用context与reducer结合,向子组件传递dispatch,而是传递回调函数,其中的一个优点也是context带来的,也就是不需要一层一层的往下传,但是这跟reducer有什么关系呢? 是否因为dispatch是永远不变的,所以通过读取它就不需要重新渲染,而回调函数是可变的,

useReducer的demo

文章Hooks, State, Closures, and useReducer 中提到了一个问题,看如下代码:

import React, { useEffect } from "react";

export default (props) => {
  const { name, age } = props;
  useEffect(() => {
    const timerId = setInterval(function log() => {
      console.log("get name", name);
    }, 3000);
    console.log("Person effect create");

    return () => {
      console.log("Person effect destroy");
      clearInterval(timerId);
    };
  }, []); // 此处没有添加依赖name

  return (
    <div>
      <p>姓名:{name}</p>
      <p>年龄: {age}</p>
    </div>
  );
};

复制代码

在useEffect中没有添加依赖name,但是在setInterval的回调函数中打印了name,当props.name更新时,useEffect不会重新执行,因为它的依赖项是空数组,这时console.log("get name", name)打印的name的值是什么?

答案:打印的是props.name第一次传进来的初始值,即使props.name更新了,console.log('get name', name)打印的始终是初始值,这里涉及到stale closure问题 ,简单的说就是:useEffect只在首次渲染时执行,setInterval中的回调函数log() 函数是一个闭包,其捕获的name是首次渲染时传进来的值,即使后续name更新了,log打印出来的name也是不变的

解决方法:在useEffect依赖项中添加name依赖,当name 更新时重新触发useEffect

 useEffect(() => {
    const timerId = setInterval(function log() => {
      console.log("get name", name);
    }, 3000);
    console.log("Person effect create");

    return () => {
      console.log("Person effect destroy");
      clearInterval(timerId);
    };
  }, [name]); // 此处没有添加依赖name

复制代码

useCallback VS useMemo

React 文档中说useCallback

Returns a memorized callback

useMemo:

Return a memorized value

简单的说就是:useCallback返回一个memorized函数,useMemo返回一个memorized 值,useCallback不会调用函数,而useMemo会调用函数并返回值

userCallback-使用场景

useCallback 通常可以配合useEffect使用,例如fetchData

const fetchUser = async () => {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/users/${userId}`
  );
  const newUser = await res.json();
  setUser(newUser); // ? setState triggers re-render
};

useEffect(() => {
  fetchUser();
}, []);   
复制代码

上面代码常用于在组件首次渲染时获取fetch data,但是有个问题:它只会被执行一次, 如果安装了

eslint-plugin-react-hooks 插件,会提示需要将fetchUser添加到依赖项中,如果添加fetchUser到userEffect的依赖项中,那么,当组件重新渲染时(如: 父组件传递的props更新导致子组件重新渲染),fetchUser的值会改变,导致useEffect的依赖更新,重新调用fetchUser,而fetchUser只需要在userId改变时调用即可,在其他情况下调用并无必要,有两种方式解决这个问题:

Opt1:

useEffect(() => {
  const fetchUser = async () => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/users/${userId}`
    );
    const newUser = await res.json();
    setUser(newUser); // ? setState triggers re-render
	};
  fetchUser()
}, [userId]);  
复制代码

这种方式有个问题是,如果在多个地方需要调用fetchUser,那么如何处理?可以采用第二种方式是

Opt2: 使用userCallback

const fetchUser = useCallback(async () => {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/users/${userId}`
  );
  const newUser = await res.json();
  setUser(newUser); // ? setState triggers re-render
}, [userId]);

useEffect(() => {
  fetchUser();
}, [fetchUser]);   
复制代码

userCallback只在userId发生改变时,才会生成新的函数赋值给fetchUser,因此,即使组件重新渲染,但是userId未改变,那么fetchUser仍然是稳定的,不会重新调用

userMemo-使用场景

对于复杂计算或者说比较耗性能的计算,都是使用userMemo进行包装,来根据依赖进行重新计算,优化性能

React.memouseMemo 主要用于替换React.PureComponent 中的shouldComponentUpdate

React.memo与React.PureComponent中的shouldComponentUpdate的区别:

  • React.memo只比较props
  • React.PureComponent中的shouldComponentUpdate会同时比较props和state

参考:

useRef

关于使用useRef有几点注意:

  • 使用const ref = React.useRef()生成的是一个对象,其有一个current的属性值,我们一般操作的是ref.current属性
  • 更新ref.current 不会导致组件重新渲染,且ref.current更新是同步的,而state更新是异步的, 这是其与state最大的区别
  • 不要在rendering(渲染)期间更新ref.current 不然会到导致一些奇怪的问题,所谓的rendering期间,对应到代码中就是组件重新渲染时,必然会执行的代码(可以理解为函数组件的immediate scope内), 最好在 effect 或 handler函数(event handler、timer handler)中更新ref.current ,查看下文更新限制
  • ref的值在重新渲染的过程中是保持不变的

使用场景1: 用于保存mutable values

也就是说可以使用ref保存带有side-effect的基础数据,这些数据不需要渲染到页面上,而使用state保存需要渲染到页面中的数据

import { useRef, useState, useEffect } from 'react';

function Stopwatch() {
  const timerIdRef = useRef(0);
  const [count, setCount] = useState(0);

  const startHandler = () => {
    if (timerIdRef.current) { return; }
    timerIdRef.current = setInterval(() => setCount(c => c+1), 1000);
  };

  const stopHandler = () => {
    clearInterval(timerIdRef.current);
    timerIdRef.current = 0;
  };

  useEffect(() => {
    return () => clearInterval(timerIdRef.current);
  }, []);

  return (
    <div>
      <div>Timer: {count}s</div>
      <div>
        <button onClick={startHandler}>Start</button>
        <button onClick={stopHandler}>Stop</button>
      </div>
    </div>
  );
}
复制代码

如上所示:使用ref保存定时器的id,在组件unmount时清除定时器,是比较合适的使用场景

使用场景2:访问DOM 元素

使用步骤分三步:

  1. 定义ref:const elementRef = React.useRef()
  2. 绑定ref到元素上: <div ref={elementRef} />
  3. 在组件渲染后,elementRef.current指向DOM,也就是在useEffect中可通过elementRef.current获取指定的元素
import { useRef, useEffect } from 'react';

function AccessingElement() {
  const elementRef = useRef();

   useEffect(() => {
    const divElement = elementRef.current;
    console.log(divElement); // logs <div>I'm an element</div>
  }, []);
	// 在此处是无法获取到dom元素的
  // const divElement = elementRef.current;
  return (
    <div ref={elementRef}>
      I'm an element
    </div>
  );
}
复制代码

更新限制:更新ref只能在effect或handler函数(event handler, timer handler)中进行

之所以要在effect或handler函数中进行ref更新,主要考虑的是在function component的immediate scope中,主要进行的hooks调用和计算输出,而不应该进行进行ref更新

这只是一个推荐执行的规则,并不是什么规范,但是,如果是获取DOM元素,那么不应该在函数组件的immediate scope中获取,因为此时还未render,所以,无法获取到DOM元素

PS: immediate scope —— 指的是函数组件内最上层的作用域,useEffect和handler函数都会形成独立的作用域,

import { useRef, useEffect } from 'react';

function MyComponent({ prop }) {
  const myRef = useRef(0);

  useEffect(() => {
    myRef.current++; // Good!

    setTimeout(() => {
      myRef.current++; // Good!
    }, 1000);
  }, []);

  const handler = () => {
    myRef.current++; // Good!
  };

  myRef.current++; // Bad!

  if (prop) {
    myRef.current++; // Bad!
  }

  return <button onClick={handler}>My button</button>;
}
复制代码

React.createRef、React.useRef、useImperativeHandle 的使用

// ---- Countdown.ts
type CountdownProps = {
}

export type CountdownHandle = {
  start: () => void,
}

const Countdown: React.RefForwardingComponent<CountdownHandle, CountdownProps> = (
  props,
  forwardedRef,
) => {
  React.useImperativeHandle(forwardedRef, ()=>({
    start() {
      alert('Start');
    }
  });

  return <div>Countdown</div>;
}

export default React.forwardRef(Countdown);
// ----- end 

// App.ts
import {CountdownHandle} from './Countdown'
const App: React.FC = () => {
  // this will be inferred as `CountdownHandle`
	// function component
  const ref = React.useRef<CountdownHandle>(null); // assign null makes it compatible with elements.
  // class component
  // ref = React.createRef<CountdownHandle>(null)
	
  // 可以在该函数组件中使用ref调用Countdown组件中使用React.useImperativeHandle提供的start方法
  return (
    <Countdown ref={ref} />
  );
};
复制代码

参考:

useImperativeHandle

主要用于向父组件暴露子组件的方法

参考:

Hooks FAQ

What can I do with Hooks that I couldn’t with classes?

Hooks提供了一种强大的、表现丰富的新的方式来重用组件之间的功能,如:使用自定义hook

参考:

Do Hooks cover all use cases for classes?

class component中有一些钩子在hook中没有“对等物”,如 getSnapshotBeforeUpdate, getDerivedStateFromErrorcomponentDidCatch

Do Hooks replace render props and higher-order components?

参考:

you-probably-dont-need-derived-state(getDerivedStateFromProps)

参考:

How do I implement shouldComponentUpdate?

可以通过React.memo来实现,但是React.memo只会比较props,不会比较state,但是我们可以提供第二个参数,也就是比较函数,该函数接收两个参数oldPropsprops,我们可以根据这两个参数自定义比较规则, 当比较函数返回true时,update会被跳过,返回false才会执行更新,使用React.memo的效果类似于使用PureComponent,但是不会比较state,PureComponent会比较

const Button = React.memo((props) => {
  // your component
}, (preProps, props) => {
  
});
复制代码

How to get the previous props or state?

可以使用useRef来实现,其实就是使用useRef生成的ref.current来保存之前的值,或者直接使用useState来保存也可以,其实这就是关于实现derived state的问题,可以如下实现:

function ScrollView({row}) {
  const [isScrollingDown, setIsScrollingDown] = useState(false);
  const [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}
复制代码

推荐用法

向子组件传递state或回调函数

通常我们可以使用props传递state(更加明确)或者将state作为context传递(对于深度更新而言更加方便,不需要一层一层的往下传递),如果选择将state作为context传递,那么对于传递给子组件的回调函数推荐使用dispatch来替代,由于dispatch是永远不变的,所以通过读取它并不会触发重新渲染,因此,最好将state和dispatch分开来作为两个context,具体查看:how to awoid passing callback down ,如下:

const TodosDispatch = React.createContext(null);

function TodosApp() {
  // Note: `dispatch` won't change between re-renders
  const [todos, dispatch] = useReducer(todosReducer);

  return (
    <TodosDispatch.Provider value={dispatch}>
      <DeepTree todos={todos} />
    </TodosDispatch.Provider>
  );
}

复制代码

那么问题是:传递dispatch和传递callback究竟 有什么区别?

推荐文章:

问题

传递callback和传递dispatch有什么区别?

官方文档说相比于传递callback,传递dispatch更好,因为dispatch是永远不变的,那么两者有什么区别?

tips

  • 可以自定义hooks在组件之间来复用状态逻辑,它可以处理大部分场景
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享