J'Blog

react hooks使用记录以及所有常用的api

react hooks使用记录以及所有常用的api

组件类

Component

所有组件都继承React.Component(props, context, updater),updater保存组件更新的方法。对于Component,react的处理逻辑很简单,实例化我们类组件,然后赋值updater对象,负责组件的更新,然后在各个阶段执行类组件的render函数,以及对应的的生命周期函数。

PureComponent

相比Component,多了个浅比较props和state,来决定是否重新渲染组件,一般用于性能优化,减少render次数。如果是普通类型发生了改变,就会重新渲染,但是如果是复杂类型,感受不到他的变化,可以使用this.setState({data: {...data})

memo

memo类似PureComponent,也是作为性能优化,区别是memo只对props的情况判断是否重新渲染组件。React.memo接收两个参数,第一个参数就是组件本身,第二个参数根据props的情况返回true/false,true表示无需渲染,false表示需要渲染,和shouldComponentUpdate中返回的true/false含义相反。

memo是个高阶组件,函数组件和类组件中都能使用。

function TextMemo(props){
    console.log('子组件渲染')
    if(props)
    return <div>hello,world</div> 
}

const controlIsRender = (pre,next)=>{
   if(pre.number === next.number  ){ // number 不改变 ,不渲染组件
       return true 
   }else if(pre.number !== next.number && next.number > 5 ) { // number 改变 ,但值大于5 , 不渲染组件
       return true
   }else { // 否则渲染组件
       return false
   }
}

const NewTexMemo = memo(TextMemo,controlIsRender)
class Index extends React.Component{
    constructor(props){
        super(props)
        this.state={
            number:1,
            num:1
        }
    }
    render(){
        const { num , number }  = this.state
        return <div>
            <div>
                改变num:当前值 { num }  
                <button onClick={ ()=>this.setState({ num:num + 1 }) } >num++</button>
                <button onClick={ ()=>this.setState({ num:num - 1 }) } >num--</button>  
            </div>
            <div>
                改变number: 当前值 { number } 
                <button onClick={ ()=>this.setState({ number:number + 1 }) } > number ++</button>
                <button onClick={ ()=>this.setState({ number:number - 1 }) } > number -- </button>  
            </div>
            <NewTexMemo num={ num } number={number}  />
        </div>
    }
}

forwardRef

forward是个高阶组件,高阶组件不能在render中使用,因为当再render中使用高阶组件时,每次render返回的高阶组件都是新创建的,与原来的一定不一致,所以每次都要重新渲染。

forwardRef使用场景:

1. 转发引入ref,比如父组件想要获取孙组件的某个dom元素,这种隔代ref获取引用,就需要forwardRef助力,解决了ref不可以通过props传递的问题

function Son (props){
    const { grandRef } = props
    return <div>
        <div> i am alien </div>
        <span ref={grandRef} >这个是想要获取元素</span>
    </div>
}

class Father extends React.Component{
    constructor(props){
        super(props)
    }
    render(){
        return <div>
            <Son grandRef={this.props.grandRef}  />
        </div>
    }
}

const NewFather = React.forwardRef((props,ref)=><Father grandRef={ref}  {...props} />  )

class GrandFather extends React.Component{
    constructor(props){
        super(props)
    }
    node = null 
    componentDidMount(){
        console.log(this.node)
    }
    render(){
        return <div>
            <NewFather ref={(node)=> this.node = node } />
        </div>
    }
}

2. 高阶组件转发ref,由于属性代理的hoc被包裹了一层,所以通过ref是拿不到原始组件的实例的,所以可以通过forwardRef转发ref

function HOC(Component){
  class Wrap extends React.Component{
     render(){
        const { forwardedRef ,...otherprops  } = this.props
        return <Component ref={forwardedRef}  {...otherprops}  />
     }
  }
  return  React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> ) 
}
class Index extends React.Component{
  componentDidMount(){
      console.log(666)
  }
  render(){
    return <div>hello,world</div>
  }
}
const HocIndex =  HOC(Index,true)
export default ()=>{
  const node = useRef(null)
  useEffect(()=>{
     /* 就可以跨层级,捕获到 Index 组件的实例了 */ 
    console.log(node.current.componentDidMount)
  },[])
  return <div><HocIndex ref={node}  /></div>
}

Lazy

React.lazy和Suspense配合一起使用,能够有动态加载组件的效果。React.lazy接受一个函数,这个函数需要动态调用import(),他返回一个Promise,该Promise需要resolve一个defalut export的React组件

import Test from './comTest'
const LazyComponent =  React.lazy(()=> new Promise((resolve)=>{
      setTimeout(()=>{
          resolve({
              default: ()=> <Test />
          })
      },2000)
}))
class index extends React.Component{   
    render(){
        return <div className="context_box"  style={ { marginTop :'50px' } }   >
           <React.Suspense fallback={ <div className="icon" ><SyncOutlined  spin  /></div> } >
               <LazyComponent />
           </React.Suspense>
        </div>
    }
}

Fragment

如果不想额外增加dom节点可以用<React.Fragment></React.Fragment>进行包裹元素

工具类

createElement

说到createElement就会想到jsx,jsx是createElement的语法糖,通过babel,用createElement最后编译成react元素形式,保存了元素的信息。createElement接收多个参数,第一个参数如果是组件类型,会传入组件,如果是dom元素类型,会传入div,p等字符串;第二个参数是一个对象,在组件类型中是props,在dom元素中是属性;其他参数,依次为children。

cloneElement

cloneElement的作用是以element元素为样板克隆并返回新的React元素。返回元素的props是将新的props和原始的props进行浅层合并后的结果。

function FatherComponent({ children }){
    const newChildren = React.cloneElement(children, { age: 18})
    return <div> { newChildren } </div>
}

function SonComponent(props){
    console.log(props)    // 此时的props就有两个属性一个是name一个是age
    return <div>hello,world</div>
}

class Index extends React.Component{    
    render(){      
        return <div className="box" >
            <FatherComponent>
                <SonComponent name="alien"  />
            </FatherComponent>
        </div>   
    }
}

createContext

createContext用于创建一个Context对象,包括用于传递Context的对象值value的Provider,和接受value变化订阅的Consumer。

使用场景包括设置全局主题,全局语言等。其次Provider是React的一个组件,利用React的context概念,将数据传递给各个组件。Provider模块的功能很简单,从最外部封装了整个应用,并向connect模块传递store, 接收到状态改变时更新store。写法:<Provider store={store}>{props.children}</Provider>

const MyContext = React.createContext(defaultValue)// createContext接收一个defaultValue,只有当Consumer上一级一直没有Provider,才会将defaultValue作为value。
function ComponentB(){
    /* 用 Consumer 订阅, 来自 Provider 中 value 的改变  */
    return <MyContext.Consumer>
        { (value) => <ComponentA  {...value} /> }
    </MyContext.Consumer>
}

function ComponentA(props){
    const { name , mes } = props
    return <div> 
            <div> 姓名: { name }  </div>
            <div> 想对大家说: { mes }  </div>
         </div>
}

function index(){
    const [ value , ] = React.useState({
        name:'alien',
        mes:'let us learn React '
    })
    return <div style={{ marginTop:'50px' }} >
        <MyContext.Provider value={value}  >
          <ComponentB />
    </MyContext.Provider>
    </div>
}

React.Children

// map
function WarpComponent(props){
    const newChildren = React.Children.map(props.children,(item)=>item)
    console.log(newChildren)
    return newChildren
} 

// foreach
function WarpComponent(props){
    React.Children.forEach(props.children,(item)=>console.log(item))
    return props.children
}   

// count
function WarpComponent(props){
    const childrenCount =  React.Children.count(props.children)
    console.log(childrenCount,'childrenCount')
    return props.children
}   

// toArray
// Children.toArray返回,props.children扁平化后结果
function WarpComponent(props){
    const newChidrenArray =  React.Children.toArray(props.children)
    console.log(newChidrenArray,'newChidrenArray')
    return newChidrenArray
}

// only
// 验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。

react hooks

useState

useState弥补了函数组件没有state的缺陷,useState可以接收一个初始值,也可以接受一个函数action,action的返回值作为新的state。返回一个数组,第一个值为state读取值,第二个值为改变state的dispatchAction函数。

useState跟类组件中的setState类似,但也有区别。

  • 在正常的react的事件流里(如onClick等同步环境下)

setState和useState是异步执行的(不会立即更新state的结果)

多次执行setState和useState,只会调用一次重新渲染render

不同的是,setState会进行state的合并,而useState则不会

  • 在setTimeout,Promise.then等异步事件中

setState和useState是同步执行的(立即更新state的结果)

多次执行setState和useState,每一次的执行setState和useState,都会调用一次render

useEffect

useEffect弥补了函数组件没有生命周期的缺陷,我们可以在useEffect第一个参数回调函数中,做一些请求数据,事件监听等操作,返回一个销毁函数可以销毁监听事件等操作;第二个参数作为dep依赖项,当依赖项发生变化,重新执行第一个函数。

useEffect可以拿来代替类组件中的componentDidMount,componentDidUpdate,componentWillUnMount这三个生命周期函数,但其实他们不是完全等价的。useEffect是在浏览器渲染结束之后才会执行的,是异步执行不会阻塞浏览器渲染;但是这三个生命周期是在浏览器渲染之前同步执行的,会阻塞浏览器的渲染;React还有个官方hook是完全等价这三个生命周期的,他叫useLayoutEffect。因为useEffect是异步的,所以如果在useEffect中修改了state值,页面其实会出现闪烁,因为渲染了两次,这种情况useLayoutEffect就能避免闪烁。

/**
 * @module 自定义hook, 解决useEffect中不能使用async的问题
 * 
 * @param effect 异步函数
 * @param dependencies []
 */
export function useAsyncEffect(effect, dependencies) {
    return useEffect(() => {
        const cleanupPromise = effect()
        return () => { cleanupPromise.then(cleanup => cleanup && cleanup()) }
    }, dependencies)
}
//为什么不能使用async,是因为,effect function 应该返回一个销毁函数(effect:是指return返回的cleanup函数),如果 useEffect 第一个参数传入 async,返回值则变成了 Promise,会导致 react 在调用销毁函数的时候报错 :function.apply is undefined

useMemo 和 useCallback

useMemo和useCallback接收的参数都是一样的,第一个参数为回调,第二个参数为依赖的数据

共同作用:当数据发生变化,才会重新计算结果,渲染,起到了缓存的作用

两者区别:useMemo计算结果是return回来的值,主要用于缓存计算结果的值,应用场景:需要计算的状态;useCallback计算结果是函数,主要用于缓存函数,应用场景:需要缓存的函数,因为函数式组件每次任何一个state的变化整个组件都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。

useMemo和useEffect的区别是useMemo返回一个缓存值,useEffect返回的是一个销毁函数;其次useMemo是在浏览器渲染时执行的,useEffect是在浏览器渲染后执行的。

举个例子:父组件传递给子组件函数属性采用匿名函数赋值,导致每次的引用地址不一样,所以不管子组件采用PureComponent还是memo进行组件优化都是没有用的,还是每次都会重新渲染,此时使用useCallback,配合memo用于优化子组件的渲染次数;useMemo是避免在每次渲染的时候进行高开销的计算,useMemo函数参数有返回值,返回的是被缓存的值。

注意:不要滥用会造成性能浪费,react中减少render就能提高性能,所以这个仅仅只针对缓存能减少重复渲染时使用和缓存计算结果。具体针对场景,比如出现state频繁更新,子组件本身不需要渲染但是由于react机制全量更新导致组件每次都重新渲染的情况可以考虑使用以上优化方式。

useRef

useRef的作用:

  • 用来获取dom元素或class组件实例
  • react-hooks原理文章中讲过,创建useRef的时候会创建一个原始对象,只要函数组件不被销毁,原始对象就会一直存在,那么我们可以通过这个特性,保存一些数据,比如定时器等

useReducer

react-hooks原理文章中讲解到,useState底层就是简单版的useReducer。

useReducer的第一个参数是一个函数,我们可以认为就是一个reducer,reducer的参数就是常规reducer中的state和action,返回改变后的state;第二参数是state的初始值。useReducer返回一个数组,第一个是更新后的state值,第二个是用来更新的dispatch函数。

useReducer可以结合useContext实现类似redux的数据流;官方的定义:在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

useContext

组件之间通信方式,组件的更新依赖另一个组件

/* 用useContext方式 */
const DemoContext = ()=> {
    const value:any = useContext(Context)
    /* my name is alien */
return <div> my name is { value.name }</div>
}
/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
    return <Context.Consumer>
         {/*  my name is alien  */}
        { (value)=> <div> my name is { value.name }</div> }
    </Context.Consumer>
}

export default ()=>{
    return <div>
        <Context.Provider value={{ name:'alien' , age:18 }} >
            <DemoContext />
            <DemoContext1 />
        </Context.Provider>
    </div>
}

useImperativeHandle

useImperativeHandle 可以配合 forwardRef 自定义暴露给父组件的实例值。对于子组件,如果是class类组件,我们可以通过ref获取类组件的实例,但是在子组件是函数组件的情况,如果我们不能直接通过ref的,那么此时useImperativeHandle和 forwardRef配合就能达到效果。优化:暴露部分数据或方法。

为了防止错用/滥用导致ref失控,React限制「默认情况下,不能跨组件传递ref」。为了破除这种限制,可以使用forwardRef。为了减少ref对DOM的滥用,可以使用useImperativeHandle限制ref传递的数据结构。

useImpreativeHandle接收三个参数:第一个参数ref(forward传递过来的ref);第二个参数createHandle处理函数,返回值作为暴露给父组件的ref对象;第三个参数deps依赖项,依赖项更改形成新的ref对象

// 我们来模拟给场景,用useImperativeHandle,使得父组件能让子组件中的input自动赋值并聚焦。
function Son (props,ref) {
    console.log(props)
    const inputRef = useRef(null)
    const [ inputValue , setInputValue ] = useState('')
    useImperativeHandle(ref,()=>{
       const handleRefs = {
           /* 声明方法用于聚焦input框 */
           onFocus(){
              inputRef.current.focus()
           },
           /* 声明方法用于改变input的值 */
           onChangeValue(value){
               setInputValue(value)
           }
       }
       return handleRefs
    },[])
    return <div>
        <input
            placeholder="请输入内容"
            ref={inputRef}
            value={inputValue}
        />
    </div>
}

const ForwarSon = forwardRef(Son)

class Index extends React.Component{
    inputRef = null
    handerClick(){
       const { onFocus , onChangeValue } =this.cur
       onFocus()
       onChangeValue('let us learn React!')
    }
    render(){
        return <div style={{ marginTop:'50px' }} >
            <ForwarSon ref={node => (this.inputRef = node)} />
            <button onClick={this.handerClick.bind(this)} >操控子组件</button>
        </div>
    }
}

useTransition

useTransition允许延时由state改变而带来的视图渲染。避免不必要的渲染。

useTransition接受一个对象, timeoutMs代码需要延时的时间

返回一个数组,第一个是一个接受回调的函数。我们用它来告诉 React 需要推迟的 state,第二个参数一个布尔值。表示是否正在等待,过度状态的完成(延时state的更新)

const SUSPENSE_CONFIG = { timeoutMs: 2000 };

function App() {
  const [resource, setResource] = useState(initialResource);
  const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG);
  return (
    <>
      <button
        disabled={isPending}
        onClick={() => {
          startTransition(() => {
            const nextUserId = getNextId(resource.userId);
            setResource(fetchProfileData(nextUserId));
          });
        }}
      >
        Next
      </button>
      {isPending ? " 加载中..." : null}
      <Suspense fallback={<Spinner />}>
        <ProfilePage resource={resource} />
      </Suspense>
    </>
  );
}

react-dom

render

render是react-dom最常用的api,用于渲染一个react元素,一般我们都用他渲染渲染根部容器app

ReactDOM.render(
    < App / >,
    document.getElementById('app')
)

hydrate

服务端渲染用hydrate,用法与 render() 相同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。

ReactDOM.hydrate(element, container[, callback])

createPortal

createPortal 可以把当前组件或 element 元素的子节点,渲染到组件之外的其他地方。

比如一些弹窗组件,一般都写在组件内,但是真正挂载的dom,是在组件外部,比如body上。

createPortal接收两个参数,第一个: child 是任何可渲染的 React 子元素 第二个: container是一个 DOM 元素。

function WrapComponent({ children }){
    const domRef = useRef(null)
    const [ PortalComponent, setPortalComponent ] = useState(null)
    React.useEffect(()=>{
        setPortalComponent( ReactDOM.createPortal(children,domRef.current) )
    },[])
    return <div> 
        <div className="container" ref={ domRef } ></div>
        { PortalComponent }
     </div>
}

class Index extends React.Component{
    render(){
        return <div style={{ marginTop:'50px' }} >
             <WrapComponent>
               <div  >hello,world</div>
            </WrapComponent>
        </div>
    }
}

unstable_batchedUpdates

在同步环境下,react有自己的批量更新机制,但是如果在异步环境下,会破坏批量更新机制,可以通过unstable_batchedUpdates来进行批量更新操作

 handerClick=()=>{
        Promise.resolve().then(()=>{
            ReactDOM.unstable_batchedUpdates(()=>{
                this.setState({ numer : this.state.numer + 1 })
                console.log(this.state.numer)
                this.setState({ numer : this.state.numer + 1 })
                console.log(this.state.numer)
                this.setState({ numer : this.state.numer + 1 })
                console.log(this.state.numer)
            }) 
        })
    }

flushSync

flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。

/* flushSync */
import ReactDOM from 'react-dom'
class Index extends React.Component{
    state={ number:0 }
    handerClick=()=>{
        setTimeout(()=>{
            this.setState({ number: 1  })
        })
        this.setState({ number: 2  })
        ReactDOM.flushSync(()=>{
            this.setState({ number: 3  })
        })
        this.setState({ number: 4  })
    }
    render(){
        const { number } = this.state
        console.log(number) // 打印什么??
        return <div>
            <div>{ number }</div>
            <button onClick={this.handerClick} >测试flushSync</button>
        </div>
    }
}

// 打印的是0,3,4,1,因为flushSync有高优先级,2和4批量更新最后render一次是4,最后异步中的1

findDOMNode

findDOMNode用于访问组件DOM元素节点,react推荐使用ref模式,不期望使用findDOMNode

unmountComponentAtNode

可以手动卸载组件包括内部的state,如果组件被移除将会返回 true ,如果没有组件可被移除将会返回 false

function Text(){
    return <div>hello,world</div>
}

class Index extends React.Component{
    node = null
    constructor(props){
       super(props)
       this.state={
           numer:1,
       }
    }
    componentDidMount(){
        /*  组件初始化的时候,创建一个 container 容器 */
        ReactDOM.render(<Text/> , this.node )
    }
    handerClick=()=>{
       /* 点击卸载容器 */ 
       const state =  ReactDOM.unmountComponentAtNode(this.node)
       console.log(state)
    }
    render(){
        return <div  style={{ marginTop:'50px' }}  > 
             <div ref={ ( node ) => this.node = node  }  ></div>  
            <button onClick={ this.handerClick } >click me</button>
        </div>
    }
}

监听localstorage变化

import { useEffect } from "react"

function safeParse(jsonString: string) {
    if (jsonString === null) {
        return null
    }
    try {
        return JSON.parse(jsonString)
    } catch (error) {
        console.error("Error parsing JSON:", error)
        return null
    }
}

function useLocalStorageListener(localStorageKey, callback) {
    useEffect(() => {
        const originalSetItem = localStorage.setItem
        localStorage.setItem = function (key, newValue) {
            const setItemEvent = new CustomEvent("setItemEvent", {
                detail: { key, newValue },
            })
            window.dispatchEvent(setItemEvent)
            originalSetItem.apply(this, [key, newValue])
        }

        const handleSetItemEvent = (event: any) => {
            const customEvent = event
            if (event.detail.key === localStorageKey) {
                // 这里的key就是本地存储对应的key
                const updatedValue = safeParse(customEvent.detail.newValue)
                callback(updatedValue) // 将本地存储最新的值传给回调函数
            }
        }

        window.addEventListener("setItemEvent", handleSetItemEvent)

        return () => {
            window.removeEventListener("setItemEvent", handleSetItemEvent)
            localStorage.setItem = originalSetItem
        }
    }, [localStorageKey, callback])
}

export default useLocalStorageListener