hook 简介
什么是hook
hook 是React 16.8 的新增特性,它可以让你在不编写class的情况下使用state以及其他React其他特性
hook官方文档:React 官方中文文档 (docschina.org)
useState
useState能让函数组件拥有自己的状态,因此,它是一个状态管理的hooks API。通过useState可以实现状态的初始化、读取、更新。
使用方式
1 2
| const [状态名, set函数] = useState(初始值)
|
案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import React,{useState} from 'react'
export const count:React.FC = ()=>{ const [count, setCount] = useState(0) return ( <> <h1> {count} </h1> <button onClick={ ()=>setCount(count+1) }> +1 </button> </> ) }
|
状态改变时,会触发函数组件的重新执行
在函数组件中使用setState定义状态之后,每当状态发生变化,都会触发函数组件的重新执行,从而根据最新数据更新渲染DOM结构。
当函数式组件被重新执行时,不会重复调用useState() 给数据赋初始值,而是复用上次的state值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import React,{useState} from 'react'
export const count:React.FC = ()=>{ const [count, setCount] = useState(0) const add = ()=>{ setCount(count+1) } return ( <> <h1>{count}</h1> <button onClick={add} >+1</button> </> ) }
|
以函数的形式为状态赋初始值
在使用useState定义状态是,除了可以直接给定初始值,还可以通过函数返回值的形式,为状态赋初始值
以函数的形式为状态赋初始值时,只有组件首次渲染才会执行useState(fn(){}) 里的fn() ; 当组件状态更新时,fn() 是不会执行的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import React,{useState} from 'react'
export const DateCom:React.FC = ()=>{ const [date, setDate] = useState( ()=>{ const dt = new Date() return {year:dt.getFullyear, month:dt.getMonth()+1, day:dt.getDate()} })
return ( <> <h1>日期</h1> <p>年:{data.year}</p> <p>月:{data.month}</p> <p>日:{data.day}</p> </> ) }
|
useState 是异步变更状态的
调用useState() 会返回一个变更状态的函数,这个函数内部是以异步形式修改状态的,所以修改状态后,无法立即拿到最新状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import React,{useState} from 'react'
export const count:React.FC = ()=>{ const [count, setCount] = useState(()=>0) const add = ()=>{ setCount(count++) console.log(count) } return( <> <h1>{count}</h1> <button onClick={add} >+1</button> </> ) }
|
解决值更新不及时的bug
当连续多次以相同的操作更新状态的值时,React内部会对传递过来的值进行比较,如果值相同,则会屏蔽后续的更新行为,从而防止组件频繁渲染的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import React,{useState} from 'react'
export const count:React.FC = ()=>{ const [count, setCount] = useState(()=>0) const add = ()=>{ setCount(count++) setCount(count++) } return ( <> <h1>{count}</h1> <button onClick={add}>+1</button> </> ) }
|
结果为每次加为1,因为setCount是异步更新状态的,前后两次调用传递的都是相同值。React内部如果两次修改时一样的,则会默认阻止组件再次更新。
解决
如果要两次都生效,我们可以使用函数的方式给状态赋新值。当函数执行时才通过函数的形参,拿到当前状态值,并基于它的返回新的状态值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import React,{useState} from 'react'
export const count:React.FC = ()=>{ count [count, setCount] = useState(()=>0) const add = ()=>{ setCount((val)=>(val+1)) setCount((val)=>(val+1)) } return ( <> <h1>{count}</h1> <button onClick={add}>+1</button> </> ) }
|
更新对象类型的值
如果要更新对象类型的值,并触发组件的重新渲染,则必须使用展开运算符或Object.assing()生成一个新对象,用新对象覆盖旧对象,才能正常触发组件的重新渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import React,{useState} from 'react'
export const UserInfo:React.FC = ()=>{ const [user, setUser] = useState({ name: 'sz', age: 10, gender: 0 }) const updateUserInfo = ()=>{ user.name = 'Jeson' setUser({...user}) setUser(Object.assign({},user)) } return ( <> <h1>用户信息</h1> <p>昵称:{user.name}</p> <p>年龄:{user.age}</p> <p>性别:{user.gender == 0 ? '女' : '男'}</p> <button onClick={updateUserInfo}>更改昵称</button> </> ) }
|
模拟组件强制刷新
在函数组件中,我们可以通过useState来模拟forceUpdate强制刷新操作。只要useState状态发生变化,就会触发函数组件的重新渲染,从而达到强制刷新目的
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import React,{useState} from 'react'
export const Update:React.FC = ()=>{ const [, forceUpdate] = useState({}) const onRefresh = ()=>forceUpdate({}) return( <> <button onClick={onRefresh}>刷新组件 - {Date.now()}</button> </> ) }
|
useRef
使用方法
useRef函数返回一个可变的ref对象,该对象只有一个current属性。可以在调用useRef函数时为其定义指定初始值。并且这个返回的ref对象在组件的整个生命周期内保持不变。
useRef函数用来解决两个问题:
- 获取DOM元素或子组件的实力对象
- 存储渲染周期之间共享的数据
1 2 3 4 5 6
| import {useRef} from 'react'
const objRef = useRef(初始值)
console.log(objRef.current)
|
获取DOM元素实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import React,{useRef} from 'react'
export const InputObject:React.FC = ()=>{ const inpRef = useRef<HTMLInputElement>(null) const getFocus = ()=>{ inpRef.current?.focus() } return ( <> <input ref={inpRef} type='text' /> <button onClick={getFocus}>点击获取input框焦点</button> </> ) }
|
存储渲染周期之间共享的数据
基于useRef创建名为prevCountRef 的数据对象,用来存储上一次的旧count值。每当点击按钮触发count自增时,都把最新的旧值赋值给prevCountRef.current 即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React,{useRef, useState} from 'react'
export const Counter:React.FC = ()=>{ const [count, setCount] = useState(0) const prevCountRef = useRef<number>() const add = ()=>{ setCount((c)=>c+2) prevCountRef.current=count } return ( <> <div>新值:{count}, 旧值:{prevCount.current}</div> <button onclick={add}>+2</button> </> ) }
|
useRef的三个注意事项
- 在组件rerender时useRef不会被重新初始化
在RefTimer组件时,通过点击+1按钮修改状态,从而触发RefTimer组件的rerender。但是RefTimer组件中的时间戳保持不变。说明组件每次渲染不会重复调用useRef函数进行初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import React,{useState, useRef} from 'react'
export const RefTimer:React.FC = ()=>{ const [count, setCount] = useState(0) const time = useRef(Date.now) console.log(Date.now) return( <> <div>useState-count:{count}</div> <div>useRef-time:{time.current}</div> <button onCliick={()=>{setCount(value=>count+1)}}>+1</button> </> ) }
|
- ref.current变化时不会造成组件的rerender
点击给ref赋新值,为time.current 赋新值,执行的结果是:
- 终端中输出了最新的time.current的值
- 没有触发RefTimer组件的rerender
说明改变ref.current不会造成组件的rerender
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import React,{useState, useRef} from 'react'
export const RefTimer:React.FC = ()=>{ const [count, setCount] = useState(0) const time = useRef(Data.now) console.log(Data.now) const updateTime = ()=>{ time.current = Data.now } return( <div>useState-count:{count}</div> <div>useRef-time:{time.current}</div> <button onCliick={updateTime}>change timRef</button> ) }
|
- ref.current不能作为其他Hooks的依赖项
由于ref.current值的变化不会造成组件的rerender,而且React也不会跟踪ref.current变化,因此ref.current不能作为其他Hooks( useMemo,useCallback,useEffect等)的依赖项
forwardRef - React提供的一个函数
ref的作用是获取实例,但由于函数组件不存在实例,因此无法通过ref获取函数组件的实例引用。而React.forwardRef就是用来解决这个问题。React.forwardRef会创建一个React组件,这个组件能够将其接受的的ref属性转发到自己的组件数
forwardRef
是 React 中用于转发 ref 的函数。当使用 forwardRef
包裹一个组件时,我们可以在父组件中通过 ref
prop 传递一个 ref 给子组件,从而访问子组件的 DOM 元素或实例。
forwardRef使用方法
1 2 3 4 5 6 7
| import React, { forwardRef, useRef } from 'react';
const MyComponent = forwardRef((props, ref) => { });
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import React, { forwardRef, useRef } from 'react'; import MyComponent from './MyComponent'
const ParentComponent:React.FC = () => { const parentRef = useRef(null);
return ( <MyComponent ref={parentRef}> This is a child component </MyComponent> ); };
|
useImperativeHandle 使用方法
直接使用ref获取DOM实例,会全面暴露DOM实例上的API,从而导致外部使用ref时有更大的自由度。在实际开发中,我们应该严格控制ref的暴露颗粒度,控制它能够调用的方法,只向外暴露主要的功能函数,其他功能不暴露
在组件中引用forwardRef获取的ref返回值为undefind。React官方提供了useImperativeHandle, 让你在使用ref时可以自定义暴露给外部那些功能或属性
使用方法
1
| useImperativeHandle( 通过forwardRef接受到的父组件ref对象, ()=>{自定义ref对象}, [依赖项数组])
|
案例
在被React.forwardRef() 包裹的组件中,需要结合useImperativeHandle Hooks API,向外暴露子组件内部成员属性和功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import React,{useRef, useState, useImperativeHandle} from 'react'
const Child = React.forwardRef((_, ref)=>{ const [count, setCount] = useState(0) const add = (step: number)=>{ setCount((val)=>(val += step)) } useImperativeHandle(ref,()=>({ count, setCount })) return ( <> <div>Child-count: {count}</div> <button onClick={()=>{add(1)}}>+1</button> </> ) })
const RefTimer:React.FC = ()=>{ const ChildRef = useRef() const showChildRef = ()=>{ console.log(ChildRef.current) ChildRef.current?.setCount(0) } return( <> <Child ref={ChildRef}></Child> <button onClick={showChildRef}>打印子组件ref信息</button> </> ) }
|
控制成员暴露的颗粒度
在Child子组件中,希望对外暴露一个将count重置为0的函数并且做过滤,而不希望直接把setCount() 暴露出去,因为父组件调用setCount() 可以传递任何参数。可以提供一个reset() 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import React, {useRef, useState, useImperativeHandle} from 'react'
const Child = React.forwardRef((_, ref)=>{ const [count, setCount] = useState(0) const add = (step:number)=>{ setCount((val)=>(val+=step)) } useImperativeHandle((ref, ()=>{ count, reset: ()=>setCount(0) })) return( <> <h3>Child</h3> <div>{count}</div> <button onclick={()=>add(+1)}>+1</button> <button onclick={()=>add(-1)}>-1</button> </> ) })
const Father:React.FC = ()=>{ const ChildRef = useRef<{count:number;reset:()=>viod}>(null) return( <> <h3>Father</h3> <Child ref={ChildRef}/> <button onClick={()=>{ChildRef.current.reset}}>重置</button> </> ) }
|
useImperativeHandle第三个参数
1 2 3 4 5 6
| useImperativeHandle(ref, creatHandle, [deps])
|
第一种用法 - 传递空数组
只有在子组件首次渲染时,执行useImperativeHandle中的fn回调,从而把return的对象作为父组件接受到的ref
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import React.{ useState, useImperativeHandle} from 'react'
const Child = React。forwardRef((_, ref)=>{ const [count, setCount] = useState(0) const add = (step:number)=>{ setCount((val)=>(val += step)) } useImperativeHandle( ref, ()=>{ console.log('只会执行一次') return{ count, reset: ()=>setCount(0) } },[]) return( <> <h3>Child</h3> <div>{count}</div> <button onCLick={()=>add(+1)}>+1</button> <button onCLick={()=>add(-1)}>-1</button> </> ) })
const Father:React.FC = ()=>{ }
|
第一种用法 - 传递依赖项数组
子组件首次渲染时和依赖项发生改变时,会执行useImperativeHandle的fn回调,从而让父组件通过ref获取到最新依赖值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import React.{ useState, useImperativeHandle} from 'react'
const Child = React。forwardRef((_, ref)=>{ const [count, setCount] = useState(0) const add = (step:number)=>{ setCount((val)=>(val += step)) } useImperativeHandle( ref, ()=>{ console.log('执行一次') return{ count, reset: ()=>setCount(0) } },[count]) return( <> <h3>Child</h3> <div>{count}</div> <button onCLick={()=>add(+1)}>+1</button> <button onCLick={()=>add(-1)}>-1</button> </> ) })
const Father:React.FC = ()=>{ }
|
第三种用法 - 不传递第三个参数
组内任何state的变化,都会导致useImperativeHandle中的回调函数重新执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import React.{ useState, useImperativeHandle} from 'react'
const Child = React。forwardRef((_, ref)=>{ const [count, setCount] = useState(0) const add = (step:number)=>{ setCount((val)=>(val += step)) } useImperativeHandle( ref, ()=>{ console.log('执行一次') return{ count, reset: ()=>setCount(0) } }) return( <> <h3>Child</h3> <div>{count}</div> <button onCLick={()=>add(+1)}>+1</button> <button onCLick={()=>add(-1)}>-1</button> </> ) })
const Father:React.FC = ()=>{ }
|
useEffect
什么时函数的副作用
函数的副作用就是函数除了返回值外,对外界环境造成其他影响,即与组件渲染无关的操作。例如获取数据,修改全局变量,更新DOM等。
useEffect是React 中的Hooks API。通过useEffect 可以执行一些副作用操作。例如:请求数据,事件监听等
使用语法
1 2 3 4 5 6 7
| useEffect(fn, deps?)
|
useEffect 的执行时机
useEffect 会在函数组件每次渲染完成后执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import React, {useEffect, useState} from 'react'
const Count:React.FC = ()=>{ const [count, setCount] = useState(0) console.log(document。querySelector('h1')?.innerHTML) const add = ()=>{ setCount(val=>(val + 1)) } useEffect(()=>{ console.log('useEffect',document。querySelector('h1')?.innerHTML) }) return( <> <h1>{count}</h1> <button onClock={()=>{add}}>+1</button> </> ) }
|
deps为依赖数组
如果想有条件的触发副作用函数的重新执行,则需要通过deps数组执行依赖项列表
React会在组件每次渲染完成后,对比渲染前后每一个依赖项的变化,只要任何一个依赖项发生变化,都会触发副作用函数的重新执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React,{useState, useEffect} from 'react'
export const Count:React.FC = ()=>{ const [count,setCount] = useState(0) const [flag, setFlag] = useState(false) const add = ()=>{ setCount((val)=>(val+1)) } useEffect(()=>{ console.log('useEffect') }, [count]) return( <> <div>{count}</div> <div>{flag}</div> <button onclick={()=>{add}}>+1</button> <button onclick={()=>{setFlag((val)=>!val}}>change flag</button> </> ) }
|
deps为空数组
如果useEffect指定一个空数组作为依赖项,则副作用只会在组件首次渲染完成后执行一次。当组件rerender时,也不会触发副作用函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React,{useState, useEffect} from 'react'
export const Count:React.FC = ()=>{ const [count,setCount] = useState(0) const [flag, setFlag] = useState(false) const add = ()=>{ setCount((val)=>(val+1)) } useEffect(()=>{ console.log('useEffect') }, []) return( <> <div>{count}</div> <div>{flag}</div> <button onclick={()=>{add}}>+1</button> <button onclick={()=>{setFlag((val)=>!val}}>change flag</button> </> ) }
|
useEffect 两个使用注意事项
- 不要在useEffect中改变依赖项值,会造成死循环
- 多个不同功能的副作用可以分开声明
清理副作用
useEffect 可以返回一个函数,用于清理副作用的回调
清理函数触发时机
- 当组件卸载时调用
- 当useEffect 副作用函数调用之前,会先执行清理函数
1 2 3 4 5 6
| useEffect(()=>{ return ()=>{} }, [依赖项])
|
实际应用场景
可以在返回的函数中清除定时器或事件监听,或者终止未完成的ajax请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import React, {useState, useEffect} from 'react'
export const MouseInfo:React.FC = ()=>{ let timerId: null | NodeJS.Timeout = null const [position, setPosition] = useState({x:0, y:0}) useEffect(()=>{ if(timerId !== null) return timerId = setTimeout(()=>{ const mouseMoveHandler = (e:MouseEvent)=>{ console.log({x:e.clientX, y:e.clientY}) setPosition({x:e.clientX, y:e.clientY}) timerId = null } },500) window.addEventListener('mousemove', mouseMoveHandler) return ()=>window.removeEventListener('mousemove', mouseMoveHandler) }, []) return( <> <div>鼠标位置:{JSON.stringify(poption)}</div> </> ) }
|
useLayoutEffect
useLayoutEffect和useEffect区别
- 用法相似
- useLayoutEffect 接受一个函数和一个依赖项数组作为参数
- 只有在数组依赖项发生改变时才会触发副作用函数
- useLayoutEffect也可以返回一个清理函数
- 区别
hooks名称 |
执行时机 |
执行过程 |
useEffect |
在浏览器重新绘制屏幕之后触发 |
异步执行,不会阻塞浏览器绘制 |
useLayoutEffect |
在浏览器重新绘制屏幕之前触发 |
同比执行,阻塞浏览器重新绘制 |
案例
点击按钮,把num值设置为0,当页面更新完后,判断num是否等于0,如果等于0,则在useEffect把num赋值为随机函数
使用useEffect 会发现,页面数字会出现闪烁效果。会先渲染为0,然后渲染随机数字。因为useEffect是浏览器绘制之后触发的
解决办法:使用useLayoutEffect,在浏览器绘制之前触发
useReducer
当状态更新逻辑较为复杂时,可以考虑使用useReducer。useReducer可以同时更新多个状态,而且能把对状态的修改从组件中独立出来。相比于useState,useReducer可以更好的描述 如何更新状态 。例如组件负责发出行为,useReducer负责更新状态。
可以让代码逻辑更清晰,代码行为更易预测
useReducer 语法格式
1 2 3 4 5 6 7 8
| const [state,dispatch] = useReducer(reducer, initState, initAction?)
|
基本使用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React,{useReducer} from 'react'
type UserType = typeof defaultState
const defaultState = {name:'zhangsan', age:16}
const reducer = (val:UserType)=>{ console.log('reducer函数') return val } export const Father:React.FC = ()=>{ const [state] = useReducer(reducer, defaultState) console.log(state) return( <> <button>修改name</button> </> ) }
|
initAction处理初始化数据 - 可选参数
定义名为 initAction 的处理函数,如果初始化数据中age为小数,负数,0,则对age进行非法值处理。在Father组件中,声明initAction函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import React, {useReducer} from 'react'
type UserType = typeof defaultState
const defaultState = {name:'zhangsan', age:16}
const reducer = (val:UserType)=>{ console.log('reducer函数') return val }
const initAction = (initState:UserType)=>{ return {...initState, age:Math.round(Math.abs(initState.age)) || 18} }
export const Father:React.FC = ()=>{ const [state] = useReducer(reducer, defaultState, initAction) }
|
案例 - 在Father组件中点击修改按钮修改name值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import React, {useReducer} from 'react'
type UserType = typeof defaultState type ActionType = { type: 'UPDATE_NAME'; payload: string } | { type: 'UPDATE_AGE'; payload: number }
const defaultState = {name:'zhangsan', age:16}
const reducer = (val:UserType, action:ActionType)=>{ console.log('reducer函数') return val switch(action.type){ case 'UPDATE_NAME': return {...val, name:action.payload} case 'UPDATE_AGE': return {...val, age:action.payload} default: return val } }
const initAction = (initState:UserType)=>{ return {...initState, age:Math.round(Math.abs(initState.age)) || 18} }
export const Father:React.FC = ()=>{ const [state, dispatch] = useReducer(reducer, defaultState, initAction) const changeUserName = ()=>{ dispatch({type: 'UPDATE_NAME', payload: '张三'}) console.log(state) } return( <> <div>{JSON.stringify(state)}</div> <button onClick={changeUserName}>修改用户名</button> </> ) }
|
案例 - 把用户信息渲染到子组件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| import React, {useReducer} from 'react'
type UserType = typeof defaultState type ActionType = { type: 'UPDATE_NAME'; payload: string } | { type: 'UPDATE_AGE'; payload: number }
const defaultState = {name:'zhangsan', age:16}
const reducer = (val:UserType, action:ActionType)=>{ console.log('reducer函数') return val switch(action.type){ case 'UPDATE_NAME': return {...val, name:action.payload} case 'UPDATE_AGE': return {...val, age:action.payload} default: return val } }
const initAction = (initState:UserType)=>{ return {...initState, age:Math.round(Math.abs(initState.age)) || 18} }
export const Father:React.FC = ()=>{ const [state, dispatch] = useReducer(reducer, defaultState, initAction) const changeUserName = ()=>{ dispatch({type: 'UPDATE_NAME', payload: '张三'}) console.log(state) } return( <> <div>{JSON.stringify(state)}</div> <button onClick={changeUserName}>修改用户名</button> <Son1 {...state}></Son1> <Son2 {...state}></Son2> </> ) }
const Son1:React.FC:<UserType> = (props)=>{ return( <> <div className='son1'> <p>{JSON.stringify(props)}</p> </div> </> ) }
const Son2:React.FC:<UserType> = (props)=>{ return( <> <div className='son2'> <p>{JSON.stringify(props)}</p> </div> </> ) }
|
通过子组件点击增加执行父组件reducer函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| import React, {useReducer} from 'react'
type UserType = typeof defaultState type ActionType = { type: 'UPDATE_NAME'; payload: string } | { type: 'UPDATE_AGE'; payload: number }
const defaultState = {name:'zhangsan', age:16}
const reducer = (val:UserType, action:ActionType)=>{ console.log('reducer函数') return val switch(action.type){ case 'UPDATE_NAME': return {...val, name:action.payload} case 'UPDATE_AGE': return {...val, age:val.age + action.payload} default: return val } }
const initAction = (initState:UserType)=>{ return {...initState, age:Math.round(Math.abs(initState.age)) || 18} }
export const Father:React.FC = ()=>{ const [state, dispatch] = useReducer(reducer, defaultState, initAction) const changeUserName = ()=>{ dispatch({type: 'UPDATE_NAME', payload: '张三'}) console.log(state) } return( <> <div>{JSON.stringify(state)}</div> <button onClick={changeUserName}>修改用户名</button> <Son1 {...state} dispatch={dispatch}/> <Son2 {...state} dispatch={dispatch}/> </> ) }
const Son1:React.FC:<UserType & {dispatch: React.Dispatch<ActionType>}> = (props)=>{ // 解构出dispatch,剩余的保存在user const {dispatch, ...user} = props const add = ()=>{ // 调用父组件dispatch dispatch({type:'UPDATE_AGE', payload:1}) } return( <> <div className='son1'> <p>{JSON.stringify(user)}</p> <!--Son1 +1--> <button onClick={add}>+1</button> </div> </> ) } // 子组件Son2 const Son2:React.FC:<UserType & {dispatch: React.Dispatch<ActionType>}> = (props)=>{ const {dispatch, ...user} = props const add = ()=>{ // 调用父组件dispatch dispatch({type:'UPDATE_AGE', payload:2}) } return( <> <div className='son2'> <p>{JSON.stringify(user)}</p> <!--Son1 +2--> <button onClick={add}>+2</button> </div> </> ) }
|
Immer - 简写reducer更新逻辑
安装immer相关依赖
1
| npm install immer use-immer --save
|
使用 useImmerReducer() 代替 useReducer()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| import React from 'react' import {useImmerReducer} from 'use-immer'
type UserType = typeof defaultState type ActionType = { type: 'UPDATE_NAME'; payload: string } | { type: 'UPDATE_AGE'; payload: number } | { type: 'REST' }
const defaultState = {name:'zhangsan', age:16}
const reducer = (val:UserType, action:ActionType)=>{ console.log('reducer函数') return val switch(action.type){ case 'UPDATE_NAME': val.name = action.payload case 'UPDATE_AGE': val.age += action.payload case 'REST': val = defaultState default: return val } }
const initAction = (initState:UserType)=>{ return {...initState, age:Math.round(Math.abs(initState.age)) || 18} }
export const Father:React.FC = ()=>{ const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction) const changeUserName = ()=>{ dispatch({type: 'UPDATE_NAME', payload: '张三'}) console.log(state) } return( <> <div>{JSON.stringify(state)}</div> <button onClick={changeUserName}>修改用户名</button> </> ) }
|
useContext
在react函数式组件中,如果组件嵌套层级很深,当父组件想把数据共享给最深的子组件时,使用传统props,一层一层往下传维护性太差。这时候我们可以使用React.createContext() + useContext() 解决多层组件数据传递问题

语法格式
- 在全局创建Context对象
- 在父组件中使用 Context.Provider 提供数据
- 在子组件中使用 useContext 使用数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import React, {useContext} from 'react'
type User = {name: string; age:number}
const Fatch:React.FC = ()=>{ const MyContext = React.createContext("初始化数据") const user:User = {name: 'zhangsan', age:22} return( <> <MyContext.Provider value={user}> <Son /> </MyContext.Provider> </> ) }
const Son:React.FC = ()=>{ const MyCtx = useContext(MyContext) return( <> <div> <p>姓名:{MyCtx.name}</p> <p>年龄:{MyCtx.age}</p> </div> </> ) }
|
以非入侵式的方式使用Context
在之前案例中,父组件使用侵入了<MyContext.Provider>
这样的代码结构
为了保证父组件使用代码的单一性,提高Provider的通用性。我们可以将Context.Provider封装到独立的Wrapper函数式组件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
type ContextType = { count:number; setCount: React.Dispatch<React.SetStateAction<number>> }
const AppContext
export const AppContextWrapper = React.FC<React.PropsWithChildren> = (props)=>{ const [count, setCount] = useState(0) return <AppContext.Provider value={{count, setCount}}>{props.children}</AppContext.Provider> }
|
1 2 3 4 5 6 7 8 9 10 11 12
| import React from 'react'
import {AppContextWrapper} from 'components/useContext/index.ts'
const App:React.FC = ()=>{ return( <AppContextWrapper> <> </AppContextWrapper> ) }
|
案例 - 使用Context 重构useReducer
将props透传改成context传递
React.memo - 使用memo函数缓存组件
当父组件被重新渲染时,也会触发子组件的重新渲染。如果子组件状态没有变化,也会被触发重新渲染。多出了无意义的性能开销
而React.memo 可以解决上述问题,防止子组件不必要的重新渲染,达到提高性能目的
基本用法
1 2
| import React from 'react' const 组件 = React.memo(函数式组件)
|
案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import React,{useState, useEffect} from 'react'
const Father:React.FC = ()=>{ const [num, setNum] = useState(0) const [count, setCount] = useState(0) return( <> <div>{num , count}</div> <button onClick={()=>setNum(num += 1)}>+1</button> <button onClick={()=>setCount(count += 1)}>+1</button> <Son num={num}></Son> </> ) }
const Son:React.FC<{num:number}> = React.memo(({num})=>{ useEffect(()=>{ console.log('子组件触发') }) return( <> <div>{num}</div> </> ) })
|
useMemo - 使用useMemo对计算的结果进行缓存 (计算属性)
当某个计算属性的值依赖于其他状态或属性,且这个计算开销较大时,可以使用useMemo来避免重复计算。它接受一个函数和依赖数组作为参数,并返回计算结果,只有依赖数组中的值发生变化时,才会重新计算结果。
基本用法
1 2 3 4 5 6 7
| const squaredCount = useMemo(() => { return count; }, [count]);
|
案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import React, { useMemo, useState } from 'react';
function App() { const [count, setCount] = useState(0);
const squaredCount = useMemo(() => { console.log('Calculating squared count'); return count ** 2; }, [count]);
return ( <div> <p>Count: {count}</p> <p>Squared Count: {squaredCount}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
export default App;
|
useCallback
useMemo hooks API 是对某个值的结果进行缓存,而useCallback 则是用来对组件内部的函数进行缓存,它返回的缓存的函数
基本用法
1 2 3 4 5 6 7 8 9
| const memoizedCallback = useCallback(callback, dependencies);
|
案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import React, { useCallback } from 'react';
function MyComponent() { const handleButtonClick = useCallback(() => { console.log('Button clicked!'); }, []);
return ( <div> <button onClick={handleButtonClick}>Click me</button> </div> ); }
|
基于React.memo和useCallback实现搜索内容联想案例
1 2 3 4 5 6 7 8 9 10 11 12
| { "code": 0, "data": [ {id: 1, word: "a1"}, {id: 2, word: "a2"}, {id: 3, word: "a3"}, {id: 4, word: "a4"}, {id: 5, word: "a5"}, ], length:5 }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| import React, { useState, useEffect, useCallback } from 'react';
const Father: React.FC = () => { const [list, setList] = useState('');
const onKwChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { setList(e.currentTarget.value); }, []);
return ( <div className="Father"> <SearchInput onChangeValue={onKwChange} /> <SearchList inputValue={list} /> </div> ); };
type SearchInputType = { onChangeValue: (e: React.ChangeEvent<HTMLInputElement>) => void; };
const SearchInput: React.FC<SearchInputType> = React.memo(({ onChangeValue }) => { return ( <input type="text" onInput={onChangeValue} /> ); });
type wordType = { id: number; word: string };
const SearchList: React.FC<{ inputValue: string }> = ({ inputValue }) => { const [list, setList] = useState<wordType[]>([]);
useEffect(() => { if (!inputValue) return; fetch('https://www.xxx.com/xxx?kw=' + inputValue) .then(res => res.json()) .then(res => setList(res.data)) .catch(e => console.log(e)); }, [inputValue]);
return ( list.map((item) => <p key={item.id}>{item.word}</p>) ); };
|
useTransition - React18 新特性
正常开发,如果一个组件渲染很慢,就会阻塞页面的其他事件交互。导致用使用体验差;useTransition 可以将一个更新转为低优先级更新,使其可以被打断,不阻塞UI对用户操作的响应,能够提高用户的使用体验。常用于视图切换时的用户体验
语法格式
1 2 3 4 5 6 7 8 9 10 11
| import {useTransition} from 'react'
function fn(){ const [isPending, startTransition] = useTransition()
}
|
使用isPending显示加载中状态;使用startTransition该操作降低优先级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import React, { useState, useTransition } from 'react';
const ExampleComponent = () => { const [isVisible, setIsVisible] = useState(false); const [startTransition, isPending] = useTransition({ timeoutMs: 2000, });
const handleClick = () => { startTransition(() => { setIsVisible(!isVisible); }); };
return ( <div> <button onClick={handleClick}> {isVisible ? 'Hide' : 'Show'} </button> {isPending ? <p>Loading...</p> : null} {/* 根据过渡状态显示加载状态 */} {isVisible ? ( <div style={{ width: 200, height: 200, background: 'red' }}></div> ) : null} </div> ); };
export default ExampleComponent;
|
使用注意事项
- 传递给
startTransition
的函数必须是同步的。React会立即执行此函数,并将在期间发生的所有状态标记为transition
。 如果在执行期间,尝试稍后执行状态更新( 例如在一个定时器中执行状态更新),这些状态更新不会被标记为transition
- 标记
transition
的状态更新将被其他状态更新打断。例如transition
中更新图表组件,并在图表组件任在重新渲染时继续在输入框中输入,React将首先处理输入框的更新,之后再重新启动对图表组件的渲染工作
transition
更新不能用于控制文本输入 - 文本输入是一个需要即时响应的用户交互操作。如果把文本输入标记为低优先级,被标记为transition的状态更新将被其他状态更新打断。会导致输入状态丢失,数据丢失
useDeferredValue - React18 新特性
useDeferredValue 提供一个state的延迟版本,根据返回的延迟的state推迟更新UI的某一部分,从而达到性能优化的目的。常见的用法是结合 Suspense
和 useTransition
来创建平滑的用户体验,比如在加载大量数据时保持页面的响应性。
语法格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React,{useState, useDeferredValue} from 'react'
export const fn:React.FC = ()=>{ const [kw, setKw] = useState('') const deferredKw = useDeferredValue(kw, { timeoutMs: 1000 }) if (deferredValue === value) { } else { } }
|
Suspense组件
Suspense
是 React 的一个组件,用于在异步加载数据时显示加载动画或其他过渡效果。
它可以与以下功能一起使用:
React.lazy
: React.lazy
是一个函数,可让你使用动态导入(Dynamic Import)的方式来定义组件的懒加载。当组件被懒加载时,可以使用 Suspense
组件来显示加载过渡效果,在组件加载完成之前显示一个占位符。
React.SuspenseList
: React.SuspenseList
是一个用于包装多个 Suspense
组件的组件,它提供了更精细的控制和配置选项,以处理多个异步加载的场景。
Suspense
组件的基本用法
- 导入
Suspense
组件:
1
| import React, { Suspense } from 'react';
|
- 在需要异步加载的组件上方使用
Suspense
组件,并设置 fallback
属性作为加载过渡元素:
1 2 3
| <Suspense fallback={<div>Loading...</div>}> {/* 异步加载的组件 */} </Suspense>
|
fallback
属性用于指定一个占位符(例如加载动画,loading 文字等),在异步组件加载完成之前会显示在页面上。
- 异步加载组件的方式可以使用
React.lazy
结合动态导入的方式:
1 2 3 4
| const MyComponent = React.lazy(() => import('./MyComponent')); /* React.lazy 接受一个函数作为参数,该函数返回一个动态导入的 Promise。在组件被实际渲染之前,这个 Promise 会被解析并加载相应的组件。 */
|
案例 - 表示内容过时
当kw的值频繁更新时,deferredKw的值会明显的滞后,此时用户在页面上看到的数据并不是最新的,为了防止用户感到困惑,我们可以给内容添加opacity透明度,表示内容过时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React,{useState, useDeferreValue} from 'react'
export const SearchBox:React.FC = ()=>{ cosnt [kw, setKw] = useState('') const deferredValue = useDeferredValue(kw)
const onInputChange = (e: React.ChangeEvent<HtmlImputElement>) =>{ setKw(e.currentTarget.value) } return( <div style={{height:500}}> <input type="text" value={kw} onChange={onInputChange}></input> <div style={{ opacity:kw !== deferredValue ? 0.3 : 1, transition: 'opacity 0.5s ease'}}> <SearchResult query={deferredValue}></SearchResult> </div> </div> ) }
|
自定义封装的hooks
封装鼠标位置
在src目录下新建 hooks/index.ts 模块,并将获取鼠标位置的代码封装成useMousePosition 的自定义hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import {useState, useEffect} from 'react'
export const MouseInfo = ()=>{ let timerId: null | NodeJS.Timeout = null const [position, setPosition] = useState({x:0, y:0}) useEffect(()=>{ if(timerId !== null) return timerId = setTimeout(()=>{ const mouseMoveHandler = (e:MouseEvent)=>{ console.log({x:e.clientX, y:e.clientY}) setPosition({x:e.clientX, y:e.clientY}) timerId = null } },500) window.addEventListener('mousemove', mouseMoveHandler) return ()=>window.removeEventListener('mousemove', mouseMoveHandler) }, []) return position }
|
在MouseInfo组件中,可以导入自己封装的kook进行使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| import React, {useState, useEffect} from 'react' import {useMousePosition} from '@/hooks/index.ts'
const MouseInfo:React.FC = ()=>{ const position = useMousePosiion() return ( <> <div>{position}</div> </> ) }
|
封装倒计时
功能分析:
- 点击一个button 调用useCountDown(5) 的hook,可以传递倒计时的秒数,如果未指定秒数则默认值为10秒,当倒计时未结束时,不能点击
- 每个1秒让秒数-1,并使用一个布尔值记录按钮是否被禁用
- 以数组的形式,向外返回每次的秒数和当前的禁用状态
在src/hooks/index.ts 模块中,封装名为 useCountDown自定义hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import {useState, useEffect} from 'react'
type UseCountDown = (seconds:number)=>[number, boolean]
export const useCountDown:UseCountDown = (second:number=10)=>{ const [count, setCount] = useState(second) const [disable, setDisabled] = useState(true) useEffect(()=>{ const timerId = setTimeout(()=>{ if(count > 1){ setCount((val)=>val-1) }else{ clearTimeout() setDisabled(false) } },1000) return ()=>clearTimeout(timerId) }, [count]) return [count, disable] }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import React from 'react' import {useCountDown} from '@/hooks/index.ts'
const CountDown:React.FC = ()=>{ const [count, disable] = useCountDown(3) return( <> <button disable={disable} onClick={()=>console.log('协议生效')}> {disable ? `请仔细阅读文档协议,请${count}秒再试` : 确认协议} </button> </> ) }
|