React Hook

122 阅读7分钟

为什么会有 hooks?

  • React Hooks 是 React 16.8 版本新增的特性

  • 让函数组件具有类组件的状态和生命周期管理能力

useState

  • setState支持stateless组件有自己的state

  • 入参:具体值或一个函数

  • 返回值:数组

    • 第一项是state值

    • 第二项负责派发数据更新,组件渲染

setState会让组件重新执行 => 配合useMemo或useCallback

const DemoState = (props) => {
   /* number为此时state读取值 ,setNumber为派发更新的函数 */
   const [number, setNumber] = useState(0) /* 0为初始值 */
   return (
     <div>
       <span>{ number }</span>
       <button onClick={ ()=> {
         setNumber(number + 1)
         console.log(number) /* 这里的number是不能够即使改变的,返回0  */
         }}
        />
     </div>
    )
}
// 当更新函数之后,state的值是不能即时改变的,只有当下一次上下文执行的时候,state值才随之改变
const a =1 
const DemoState = (props) => {
   /*  useState 第一个参数如果是函数 则处理复杂的逻辑,返回值为初始值 */
   let [number, setNumber] = useState(()=>{
      // number
      return a === 1 ? 1 : 2
   }) /* 1为初始值 */
   return (<div>
       <span>{ number }</span>
       <button onClick={ ()=>setNumber(number+1) } ></button>
   </div>)
}

useEffect

  • 使用条件

    • 当组件init、dom render完成

    • 操纵dom

    • 请求数据(如componentDidMount)

  • 不限制条件,组件每次更新都会触发useEffect => componentDidUpdate 与 componentwillreceiveprops

  • 第一个参数为处理事件,第二个参数接收数组,为限定条件

    • 当数组变化时触发事件,为[]只在组件初始化时触发
  • 第一个参数有返回时 => 消除副作用

    • 去除定时器、事件绑定
/* 模拟数据交互 */
function getUserInfo(a){
  return new Promise((resolve)=>{
    setTimeout(()=>{ 
       resolve({
           name:a,
           age:16,
       }) 
    },500)
  })
}

const Demo = ({ a }) => {
  const [ userMessage , setUserMessage ] = useState({})
  const [number, setNumber] = useState(0)
  
  const div= useRef()
  
  const handleResize =()=>{}

  useEffect(()=>{
     getUserInfo(a).then(res=>{
         setUserMessage(res)
     })
     console.log(div.current) /* div */
      window.addEventListener('resize', handleResize)
  /* 
     只有当props->a和state->number改变的时候 ,useEffect副作用函数重新执行 ,
     如果此时数组为空[],证明函数只有在初始化的时候执行一次相当于componentDidMount
  */
  },[ a ,number ])

  return (<div ref={div} >
      <span>{ userMessage.name }</span>
      <span>{ userMessage.age }</span>
      <div onClick={ ()=> setNumber(1) } >{ number }</div>
  </div>)
}
const Demo = ({ a }) => {
  const handleResize = () => {}
  useEffect(() => {
    const timer = setInterval(() => console.log(666), 1000)
    window.addEventListener('resize', handleResize)

    /* 此函数用于清除副作用 */
    return function () {
      clearInterval(timer)
      window.removeEventListener('resize', handleResize)
    }
  }, [a])
  return <div></div>
}

useEffect无法直接使用async await

useEffect(() => {
  const fetchData = async () => {
    const data = await fetch('https://xxx.com')
    const json = await response.json()

    setData(json)
  }

  // call the function
  fetchData()
    // make sure to catch any error
    .catch(console.error)
}, [])

监听多个属性变化

import React, { useEffect } from 'react'

function MyComponent({ prop1, prop2 }) {
  useEffect(() => {
    // 当 prop1 或 prop2 发生变化时执行的代码
    console.log('prop1 或 prop2 发生了变化')

    // 在这里可以执行你想要的操作,比如发送网络请求、更新状态等等
    if (prop1 && prop2) {
      // 当 prop1 和 prop2 都满足条件时执行的代码
      console.log('prop1 和 prop2 都发生了变化')

      // 在这里可以执行你想要的操作,比如发送网络请求、更新状态等等
    }
  }, [prop1, prop2])

  return (
    // 组件的 JSX
    111
  )
}

export default MyComponent

useLayoutEffect

渲染更新之前的 useEffect

  • useEffect

    • 组件更新挂载完成 -> 浏览器dom 绘制完成 -> 执行useEffect回调
  • useLayoutEffect

    • 组件更新挂载完成 -> 执行useLayoutEffect回调-> 浏览器dom 绘制完成
  • 渲染组件

    • useEffect:闪动

    • useLayoutEffect:卡顿

// 用 useLayoutEffect 不能看出来 dom 的变化
const DemoUseLayoutEffect = () => {
  const target = useRef()
  useLayoutEffect(() => {
      /*我们需要在dom绘制之前,移动dom到制定位置*/
      const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
      animate(target.current,{ x,y })
  }, []);
  return (
    <div >
      <span ref={ target } className="animate"></span>
    </div>
  )
}

useContext

  • 用来获取父级组件传递过来的context值

    • 这个当前值就是最近的父级组件 Provider 的value
  • 从parent comp获取ctx方式

    • useContext(Context)

    • Context.Consumer

/* 用useContext方式 */
const DemoContext = () => {
  const value = useContext(Context)
  /* my name is aaa */
  return <div> my name is {value.name}</div>
}

/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
  return <Context.Consumer>
    {/*  my name is aaa  */}
    { (value)=> <div> my name is { value.name }</div> }
  </Context.Consumer>
}

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

useReducer

  • 入参

    • 第一个为函数,可以视为reducer

      • 包括state 和 action
    • 返回值 state (base on action)

    • 第二个为state的初始值

  • 出参

    • 第一个是更新后的state值

    • 第二个是派发更新的dispatch函数

      • 执行dispatch会导致组件re-render

        • (另一个是useState)
  • useReducer+useContext 代替Redux

const DemoUseReducer = () => {
  /* number为更新后的state值,  dispatchNumber 为当前的派发函数 */
  const [number, dispatchNumber] = useReducer((state, action) => {
    const { payload, name } = action
    /* return的值为新的state */
    switch (name) {
      case 'a':
        return state + 1
      case 'b':
        return state - 1
      case 'c':
        return payload
    }
    return state
  }, 0)
  return (
    <div>
      当前值:{number}
      {/* 派发更新 */}
      <button onClick={() => dispatchNumber({ name: 'a' })}>增加</button>
      <button onClick={() => dispatchNumber({ name: 'b' })}>减少</button>
      <button onClick={() => dispatchNumber({ name: 'c', payload: 666 })}>赋值</button>
      {/* 把dispatch 和 state 传递给子组件  */}
      <MyChildren dispatch={dispatchNumber} State={{ number }} />
    </div>
  )
}

useMemo

  • 根据useMemo的第二个参数deps(数组)判定是否满足当前的限定条件来决定是否执行第一个cb
// selectList 不更新时,不会重新渲染,减少不必要的循环渲染
useMemo(() => (
  <div>{
    selectList.map((i, v) => (
      <span
        className={style.listSpan}
        key={v} >
        {i.patentName} 
      </span>
    ))}
  </div>
), [selectList])
// listshow, cacheSelectList 不更新时,不会重新渲染子组件
useMemo(() => (
    <Modal
      width={'70%'}
      visible={listshow}
      footer={[
        <Button key="back">取消</Button>,
        <Button key="submit" type="primary">
          确定
        </Button>,
      ]}
    >
      {/* 减少了PatentTable组件的渲染 */}
      <PatentTable
        getList={getList}
        selectList={selectList}
        cacheSelectList={cacheSelectList}
        setCacheSelectList={setCacheSelectList}
      />
    </Modal>
  ),
  [listshow, cacheSelectList]
)
// 减少组件更新导致函数重新声明
const DemoUseMemo = () => {
  /* 用useMemo 包裹之后的log函数可以避免了每次组件更新再重新声明 ,可以限制上下文的执行 */
  const newLog = useMemo(() => {
    const log = () => {
      console.log(123)
    }
    return log
  }, [])
  return <div onClick={() => newLog()}></div>
}
// 如果没有加相关的更新条件,是获取不到更新之后的state的值的
const DemoUseMemo = () => {
  const [number, setNumber] = useState(0)
  const newLog = useMemo(() => {
    const log = () => {
      /* 点击span之后 打印出来的number 不是实时更新的number值 */
      console.log(number)
    }
    return log
    /* [] 没有 number */
  }, [])
  return (
    <div>
      <div onClick={() => newLog()}>打印</div>
      <span onClick={() => setNumber(number + 1)}>增加</span>
    </div>
  )
}

useCallback

useMemo返回cb的运行结果

useCallback返回cb的函数

import React, { useState, useCallback } from 'react'

function Button(props) {
  const { handleClick, children } = props;
  console.log('Button -> render');
  return (
      <button onClick={handleClick}>{children}</button>
  )
}

const MemoizedButton0 = React.memo(Button);

export default function Index() {
  const [clickCount, increaseCount] = useState(0);

  const handleClick = () => {
      console.log('handleClick');
      increaseCount(clickCount + 1);
  }
  return (
      <div>
          <p>{clickCount}</p>
          <MemoizedButton0 handleClick={handleClick}>Click</MemoizedButton0>
      </div>
  )
}
// MemoizedButton0还是重新渲染了
// Index组件state发生变化,导致组件重新渲染
// 每次渲染导致重新创建内部函数handleClick
// 进而导致子组件Button也重新渲染

import React, { useState, useCallback } from 'react'

function Button(props) {
  const { handleClick, children } = props;
  console.log('Button -> render');
  return (
      <button onClick={handleClick}>{children}</button>
  )
}

const MemoizedButton1 = React.memo(Button);

export default function Index() {
  const [clickCount, increaseCount] = useState(0);
  // 这里使用了`useCallback`
  const handleClick = useCallback(() => {
      console.log('handleClick');
      increaseCount(clickCount + 1);
  }, [])

  return (
      <div>
          <p>{clickCount}</p>
          <MemoizedButton1 handleClick={handleClick}>Click</MemoizedButton1>
      </div>
  )
}

实战

是否所有依赖都要放在依赖数组中

  • 该变量变化时,需要触发 useEffect 函数执行 => 把变量放到 deps 数组中

尽量不要用useCallback

  • useCallback 大部分场景没有提升性能

  • useCallback让代码可读性变差

useMemo建议适当使用

  • 在deps不变,且非简单的基础类型运算的情况下建议使用

useState的正确使用姿势

  1. 能用其他状态计算出来就不用单独声明状态

    • 一个 state 必须不能通过其它 state/props 直接计算出来,否则就不用定义 state
  2. 保证数据源唯一,在项目中同一个数据,保证只存储在一个地方

  3. useState 适当合并

自定义Hooks - 本质:实现一个函数

setTitle hook

import { useEffect } from 'react'

const useTitle = (title) => {
  useEffect(() => {
    document.title = title
  }, [])

  return
}

export default useTitle

const App = () => {
  useTitle('new title')
  return <div>home</div>
}

update hook

import { useState } from 'react'

const useUpdate = () => {
  const [, setFlag] = useState()
  const update = () => {
    setFlag(Date.now())
  }
  return update
}

export default useUpdate

// 实际使用
const App = (props) => {
  // ...
  const update = useUpdate()
  return (
    <div>
      {Date.now()}
      <div>
        <button onClick={update}>update</button>
      </div>
    </div>
  )
}

useScroll hooks

import { useState, useEffect } from 'react'

const useScroll = (scrollRef) => {
  const [pos, setPos] = useState([0, 0])

  useEffect(() => {
    function handleScroll(e) {
      setPos([scrollRef.current.scrollLeft, scrollRef.current.scrollTop])
    }
    scrollRef.current.addEventListener('scroll', handleScroll)
    return () => {
      scrollRef.current.removeEventListener('scroll', handleScroll)
    }
  }, [])

  return pos
}

export default useScroll

// 用法
import React, { useRef } from 'react'
import { useScroll } from 'hooks'

const Home = (props) => {
  const scrollRef = useRef(null)
  const [x, y] = useScroll(scrollRef)

  return (
    <div>
      <div ref={scrollRef}>
        <div className="innerBox"></div>
      </div>
      <div>
        {x}, {y}
      </div>
    </div>
  )
}

Hooks VS HOC

  1. Hook

    • 把更相关的逻辑放在一起 => 取代掉生命周期

    • 适合做 Controller 或者需要内聚的相关逻辑

  2. 高阶组件

    • 将外部的属性功能放到一个基础 Component 中 => 扩展能力的插件

      • react-swipeable-views中的 autoPlay 高阶组件

      • 通过注入状态化的 props 的方式对组件进行功能扩展,而不是直接将代码写在主库中)

react hooks为什么不能放在if和for里?

  • React 顺序调用 Hook

  • 在 if 和 for 中的执行次数是动态的,可能会导致 Hook 的调用顺序发生改变,从而引发问题

    • 在某个循环中使用 useState Hook,由于循环次数是不定的,Hook 的调用顺序也就无法确定,会导致状态更新混乱。
  • 只能在函数组件的顶层作用域中使用 Hook