【react框架】React18 Hooks:我的避坑指南(下)

前言

React的hooks我的理解是分为两个类型,一个是原生的hooks,一个是后期我们开发人员自己封装的自定义hooks。

原生的hooks提供了React的基本功能,且使用起来也非常顺手。但我在日常开发中发现了这些hooks在使用上有非常多的注意事项,或者叫做坑,而且有些还挺容易忘记的,所以写个博客记录下。

这篇还会讲我对自定义hook的一些心得~

上篇地址

文章更新记录:简化篇幅,优化记忆点


useLayoutEffect

功能和使用方式useEffect一样,不过有点区别。

了解Fiber架构的应该知道,React的JS都是在DOM渲染的间隔时间“见缝插针”中执行的。而useEffect副作用中的逻辑是会被作为异步任务去执行的,也就是说有可能上个“任务缝隙”中执行了useEffect,要等下个“任务缝隙”才能执行副作用的逻辑。

而useLayoutEffect是把副作用作为同步任务了,也就是一定会在一个“任务缝隙”中全部执行完,才交给DOM渲染。

可以看这篇文章的举例

文中的例子可以看为:

  • 用useEffect时:设置为0被useEffect监听执行—DOM更新页面为0—useEffect副作用执行改为随机数—DOM更新页面为随机数
  • 使用useLayoutEffect时:设置为0被useLayoutEffect监听执行,副作用执行改为随机数—DOM更新页面为随机数

memo

这个不是hook,但和下面两个hook有关系,所以必须讲。

作用是让传入子组件的状态如果没更新的话,父级组件里的其他组件或者标签无论怎么更新都不会让它render一次。

import React, { useState, useEffect, memo } from 'react';

function Search(props) {
  // ...
  return (
    <div>
    </div>
  )
}

function areEqual(prevProps, nextProps){ 
  // console.log(prevProps, nextProps) // 入参能拿到所有变量的集合,第一个是改变前,第二个是改变后
  if (xxx) { // 这里可以细化控制,设置某个条件下,返回ture不更新组件,返回false更新组件
  	return true 
  } else {
    return false
  }
}

export default memo(Search, areEqual); // 包裹组件,第二个参数可以默认不传,如果想控制的细一些可以自己写函数再传

一般用的时候第二个参数不用传函数,react底层已经默认帮我们处理好了,不过好像它内部只会做一维的比较。

为什么react在设计的时候不默认给每个组件都加上这个东西呢?每次一点小变化都触发整个函数组件更新。其实这个机制是可以拿来利用的,给你写个例子:

const TitleElem: FC = () => {
  const [editState, SetEditState] = useState(false)

  if (editState) {
    return (
      <Input/>
    )
  }

  return (
      <Button type="text" onClick={() => SetEditState(true)} />
  )
}

看到了吗直接在函数组件的第一层作用域下使用条件语句返回不同jsx。


useMemo和useCallback

这俩就像vue的计算属性一样。我们知道当state更新了,整个函数组件都会重新执行一遍,变量和函数都会重新定义。如果我们使用了这俩个hooks,当所依赖的变量未发生变化时,是不会重新执行定义的。

const [a, seta] = useState(1)
const [b, setb] = useState(2)

const sum = useMemo(() => a + b, [a, b]) // 定义变量
const getA = useCallback(() => { seta(a+1) }, [a]) // 定义函数

旧的官网上说不能保证useMemo能一定保证每一次的缓存

如果依赖值不填,那么只会在组建第一次创建的时候去创建这些包裹的东西,后续更新就不会再重新创建了,拿到的永远是初始状态。当然,如果包裹里用到了响应式数据,需要填入依赖值,防止闭包陷阱问题(详见useEffect用法)。

useCallback第一个能想到的场景就是当一个子组件使用了memo,然后父组件这样用:

const [go, setGo] = useState('我很淘气')
const [a, seta] = useState(1)
const add = () => {
	seta(a+1)
}
return (
	<Count addFn={add}></Count>
)

这个时候如果go变量被修改,导致父组件整个重新更新,那么add重新被定义,addFn被迫重新赋值,此时子组件的memo就没用了,被迫做无用的更新

这时候改成:

const [go, setGo] = useState('我很淘气')
const [a, seta] = useState(1)
const add = useCallback(() => {
	seta(a+1)
}, [a])
return (
	<Count addFn={add}></Count>
)

如果go变量被修改,导致父组件整个重新更新,但是a变量又没有变化,所以add不变,所以addFn不变,子组件不更新。

还有一个场景是,例如你的某个函数aFn被useEffect中使用且监听,那么可以用useCallback包裹下。

useMemo的话,第一个场景和useCallBack一样的。第二个使用场景是当要用一个函数return jsx的时候,可以useMemo包裹,因为我们返回的这个jsx不一定经常变动,其他响应式变量变化了,也不想让他重新执行一遍,那用useMemo就很方便了。

所以对于他俩的第一个使用场景,如果是一些和组件能解耦的变量或者函数,可以直接写在函数组件定义外层,就没必要用他俩来包裹了。

tips:给子组件传入setGo、seta这种setData函数,是不会重新触发子组件更新的,因为就算父组件更新,setData函数也不会被重新定义。

不可滥用

他俩不能滥用!

第一:本身他俩的使用其实也带有一定的消耗。

第二:当组件变得越来越复杂,并且内部使用到他俩的时候,数组中的依赖值有可能会变多,容易造成一个“失效”的问题。

例如:

const [a, seta] = useState(1)
const add = useCallback(() => {
	seta(a+1)
}, [a])
return (
	<Count addFn={add} seta={seta}></Count> // Count内部用memo包裹
)

当子组件内部的一个按钮会同时执行add和seta时,父组件的add会被重新定义,那么子组件就会重新渲染,那么useCallback用的就没有意义了。

这个时候你应该就会用useEffect去做改造写法,但是但组件逻辑变复杂的时候,改造往往会消耗大量精力。这也是现阶段18版本不好的地方。

听说19版不会再让我们使用memo、useCallback、useMemo,底层会自动帮我们处理。


路由相关hooks

【react框架】学习记录12-React Router 6的学习使用记录


useImperativeHandle

我常用来限制子组件对外暴露的方法,可以参考:react useImperativeHandle 的最佳实践及原理


useReducer与useContext

useReducer的用法可以理解为useState的“复杂版,当set状态时要经过不同类型的方式处理,用useReducer就很合适。

不过我没见过有人会这么写,所以我这里就不记录了,和useContext放在一起讲。

useContext常用于传递数据。我举个例子,不一定参考我这个。

首先创建通信组件userContext.js:

import React, { useReducer } from 'react';

const initState = { // 这是state
  isLogin: false,
  user: {
    id: '007',
    name: '詹姆斯'
  }
};

const UserContext = React.createContext(); // 创建容器

const reducer = (state, action)=>{ // 定义reducer方法,根据传入的动作类型,返回新的state
  switch (action.type) {
    case 'LOGIN':
      return {
        ...state,
        isLogin: action.payload
      }
  	// ...
    default:
      break;
  }
};

const UserContextProvider = (props)=>{ // 函数组件
  const [state, dispatch] = useReducer(reducer, initState); // 创建reducer,把修改值的方法和state传入,返回state和触发派发动作的函数
  return (
    <UserContext.Provider value={{state, dispatch}}> // 固定写法
      {props.children}
    </UserContext.Provider>
  )
}

export {
  UserContext,
  UserContextProvider
}

使用,在父级组价中实例化通信组件index.js:

import React from 'react';
import { UserContextProvider } from './userContext';
import App from './app'; // 引入个子组件

export default function(props){
  return (
    <UserContextProvider> // 必须把只组件包裹起来
      <App {...props}/> // 将userContext组件的数据以props形式传入
    </UserContextProvider>
  )
}

子组件App.js中使用通信组件:

import React, { useContext } from 'react';
import User from './user'; // 子组件
import { UserContext } from './userContext';
import { Button } from 'antd-mobile';

export default function(props){
  const {state, dispatch} = useContext(UserContext); // 获取通信组件的内容

  const handleLogin = ()=>{
    dispatch({
      type: 'LOGIN',
      payload:  true
    })
  }

  return (
    <div>
      {state.isLogin ? <User /> : <Button type='primary' onClick={handleLogin}>登陆</Button>}
    </div>
  )
}

孙子组件使用通信组件:

import React, { useContext } from 'react';
import { UserContext } from './userContext';

export default function(props){
  const { state } = useContext(UserContext);
  return (
    <div>
      <h1>user: </h1>
      <h1>user-id: {state.user.id}</h1>
      <h1>user-name: {state.user.name}</h1>
    </div>
  )
}

槽点:这玩意每次写起来都很麻烦。


自定义hook的一些心得

相较于类组件的高阶组件写法(难记加难用),自定义hooks抽离公共代码的方式友好多了。

hooks与普通的公共函数

看网上说hooks的初学者会分不清一个公共的功能应该封成一个hook还是函数,我目前的理解是:

  1. 当需要使用到react能力的情况下,就使用hook;
  2. 当用hooks能比用普通函数使用更简便代码更少的情况下,就使用hooks;
  3. 当你想把开发思维转向react hook的时候,就尽量第一个想到用hook实现;

第一个好理解一些,说说第二个什么意思,拿个修改页签标题的hooks来举例,这个功能也可以使用普通函数去实现,但是这样的话,在每次修改标题变量的时候,还需要触发函数去修改,多了一个触发动作,而使用hooks就不用关心这个问题。

第三点是我从vue2转过来的时候发现的,其实react的开发思维和vue是不同的,我一下子很难用语言去描述出来,只能靠多实践多思考去体会了。

只能在最高作用域上调用

这点是最不爽的,自定义hook只能在和useState同等作用域下使用。

目前个人的解决办法是,hook返回一个执行函数,这个函数调用可以重新执行hook里的逻辑。

还是举个例子吧,防止小伙伴不明白啥意思。例如写个修改页签标题的hook:

import { useLayoutEffect, useState } from "react";

export default function useChangeTitle(title) {
  const [state, setState] = useState();

  useLayoutEffect(() => {
    changeTitle(title)
  }, [title]);

  const changeTitle = (title) => {
    document.title = title;
    setState(title);
  };

  return { state, changeTitle };
}

看到了没,我把修改标题的方法也返回出去了,引用的时候就可以这样:

import useChangeTitle from "../../hooks/useChangeTitle";

export default function App() {
  let { state: title, changeTitle } = useChangeTitle('标题1')
  const fn = () =>{
    console.log('之前标题', title);
    changeTitle('标题2') // 重新执行了hook里的逻辑
  }

  return <div onClick={()=>{
    fn()
  }}>App</div>;
}

关于hooks的导出建议

推荐在放hooks的文件夹中,创建一个负责统一导出的文件index.js

export { default as useTitleHook } from './useTitleHook';

然后使用hooks的时候:

import { useTitleHook } from '@/hooks' // 位置写法自己去webpack定义

此时,如果新人想看useTitleHook的内部代码,ctrl加鼠标是没法点的,需要在根目录创建一个文件jsconfig.json:

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@/hooks": ["hooks/index"]
    }
  }
}

重启编辑器即可。

尽量使用第三方hooks

你会发现平时抽离出来的功能性hooks其实其他的开发人员也会用到,自己写的终究是没有市面上成熟的库里提供的好,所以平时更推荐使用第三方的hooks。

推荐两个:

所以平时写hooks的时候,如果是功能类的,优先看看库里有没有实现。自定义hooks尽量都写与业务绑定的功能

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值