文章目录
前言
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还是函数,我目前的理解是:
- 当需要使用到react能力的情况下,就使用hook;
- 当用hooks能比用普通函数使用更简便代码更少的情况下,就使用hooks;
- 当你想把开发思维转向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。
推荐两个:
- 首推ahooks,中文化做的好些 https://ahooks.js.org/zh-CN/
- react-use,这个库主要是国外在用 https://github.com/streamich/react-use
所以平时写hooks的时候,如果是功能类的,优先看看库里有没有实现。自定义hooks尽量都写与业务绑定的功能