React Hooks

257 阅读14分钟

一、React Hooks介绍

1、hook是react 16.8的新增特性,它可以让开发者在不编写class的情况下使用state以及其他的react特性

2、hook的使用规则

①class组件中不能使用hook

  constructor(props) {
    super(props)
    const [count, setCount] = useState(0)
  }

package.json中关闭react-hooks/rules-of-hooks提示

  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ],
    "rules": {
      "react-hooks/rules-of-hooks": "off"
    }
  },

页面中会报语法错误

②只能在函数最外层调用,不能在循环、条件判断、子函数中调用

  if (count < 3) {
    useEffect(() => {
      console.log({ count })
    })
  }

这样写会破坏hook调用的顺序:当count大于等于3时,此时的useEffect的调用顺序被改变,react会报错。create-react-app创建的项目中,内置了react-hooks/rules-of-hooks进行hook语法校验,默认为开启状态

react怎么知道哪个state对应哪个useState?----react靠的是hook调用的顺序。所以我们在使用hook时,一定要确保hook的调用顺序在每次渲染中都是相同的

③只能在react的函数式组件或者自定义hook函数中调用hook,不能在其他JavaScript函数中调用

函数式组件和普通函数的区别:函数式组件会return一个dom。

1、使用create-react-app创建项目

npx create-react-app react_hooks // node版本14及以上,我的是14.16.0

要注意,最新版的react已经到18.2.0了,需要手动改为17版本

    "react": "^17.0.1",
    "react-dom": "^17.0.1",

同时入口文件(index.js)中格式相应的调整

import React from 'react';
// import ReactDOM from 'react-dom/client';
import ReactDOM from 'react-dom'
import App from './App';
// import reportWebVitals from './reportWebVitals';

// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(
//   <React.StrictMode>
//     <App />
//   </React.StrictMode>
// );
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
// reportWebVitals();

2、类式组件和函数式组件(hooks)的编写形式对比

类式组件【Count】:

import React, { Component } from 'react'

export default class Count extends Component {
  // constructor(props) {
  //   super(props) // 如果写constructor,必须写super;如果希望构造函数中通过this访问props,就将props传入
  //   this.state = { count: 0 }
  // }
  state = { count: 0 } // 类式组件中构造函数能不写就不写

  render() {
    return (
      <>
        <p>点击了{this.state.count}次</p>
        <button onClick={this.add.bind(this)}>点击</button>
      </>
    )
  }
  add() {
    this.setState({ count: this.state.count + 1 })
  }
}

函数式组件【Count1】:

import React, { useState } from 'react'

const Count1 = () => {
  const [count, setCount] = useState(0)

  const add = () => {
    setCount(count + 1)
    // setCount((count) => count + 1)
  }
  return (
    <>
      <p>点击了{count}次</p>
      <button onClick={add.bind(this)}>点击</button>
    </>
  )
}

export default Count1

二、useState

1、useState的介绍

useState是react自带的一个hook函数,它的作用是用来声明状态变量

(1)声明方式

  const [count, setCount] = useState(0)

这句代码使用了es6的数组解构,等价于

  const _useState = useState(0)
  const count = _useState[0]
  const setCount = _useState[1]
  1. useState函数接收的参数是状态的初始值,它返回一个数组,数组的第0位是当前的状态值,第1位是可以改变状态值的方法函数
  2. 惰性state:useState中的参数只会在组件初始化时起作用,后续渲染时会被忽略
  3. 复杂初始state可以通过传入函数,在函数中计算并返回初始值
  const [num, setNum] = useState(() => (props.num > 100 ? props.num : 0))

(2)如何读取状态中的值

      <p>点击了{count}次</p>

count就是js中的一个变量,想要在jsx中使用,值用 {} 包裹就可以

(3)如何改变state的值

    setCount(count + 1)
    setCount(() => count + 1)

如果定义的初始数据是引用类型,使用扩展运算符

    const [person, setPerson] = useState({name: '小明', age: 18})
    setPerson(() => ({ ...person, name: '大明' }))
  • 直接调用setCount函数,这个函数接收的参数是修改过的新状态值。接下来的事情就交给react,它会重新渲染组件
  • react自动帮助我们记忆了组件的上一次状态值,虽然这种记忆也会有麻烦,但是遵循它这种规则,就可以愉快的编码

2、useState()比this.setState()效率高

  • 在类式组件中,只要执行了this.setState(),即使不改变数据,也会触发render,导致效率低。需要通过shouldComponentUpdate钩子或使用PureComponent替代Component进行优化
  • 在函数式组件中,如果调用useState的更新函数时没有改变数据,那么react将跳过子组件的渲染及effect的执行,底层使用Object.is比较算法来比较state

三、useEffect

1、useEffect代替常用生命周期函数componentDidMount和componentDidUpdate

(1)类式组件为计时器添加生命周期函数,componentDidMountcomponentDidUpdate分别在初次渲染和计数器发生改变后打印count

import React, { Component } from 'react'

export default class Count extends Component {
  state = { count: 0 }

  componentDidMount() {
    console.log('componentDidMount', this.state.count)
  }

  componentDidUpdate() {
    console.log('componentDidUpdate', this.state.count)
  }

  render() {
    return (
      <>
        <p>点击了{this.state.count}次</p>
        <button onClick={this.add.bind(this)}>点击</button>
      </>
    )
  }
  add() {
    this.setState({ count: this.state.count + 1 })
  }
}

(2)用useEffect代替这两个生命周期函数

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

const Count1 = () => {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('小明')

  useEffect(() => {
    console.log('useEffect', count) // 任意一个state发生变化,都会触发这里
  })

  return (
    <>
      <p>姓名:{name}</p>
      <p>点击了{count}次</p>
      <button onClick={() => setCount(count + 1)}>点击</button>
      <button onClick={() => setName('xx')}>改变名字</button>
    </>
  )
}

export default Count1

注意点:

  • react初次渲染(DOM更新后)和之后的每次渲染都会调用useEffect函数,之前需要用componentDidMount和componentDidUpdate两个钩子来完成
  • useEffect中定义的函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而componentDidMount和componentDidUpdate中的代码都是同步执行的

2、useEffect代替componentWillUnmount生命周期函数

通常,组件卸载时需要清除effect创建的定时器id或订阅等资源,在类式组件中通过componentWillUnmount钩子(该钩子可以理解成解绑副作用)完成,在函数式组件中通过useEffect函数中返回一个函数,在返回的函数(该函数的作用类似于componentWillUnmount)中进行清除资源

写个路由切换的demo体会下:

动图.gif (1)安装react-router-dom

npm i react-router-dom@5.2.0

(2)src下创建Home组件

const Home = () => {
  return <h1>Home</h1>
}

export default Home

(3)Count1组件改一下

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

const Count1 = () => {
  const [count, setCount] = useState(0)
  const [date, setDate] = useState(`今天是:${new Date()}`)

  useEffect(() => {
    console.log('useEffect', count)
  }, [])

  useEffect(() => {
    const timer = setInterval(() => {
      setDate(`今天是:${new Date()}`)
    }, 1000)
    return () => {
      console.log('销毁前', timer)
      clearInterval(timer) // 这里相当于componentWillUnmount钩子,可以做一些清除资源的操作,清除定时器和取消订阅
    }
  }, [])

  return (
    <>
      <p>日期:{date}</p>
      <p>点击了{count}次</p>
      <button onClick={() => setCount(count + 1)}>点击</button>
    </>
  )
}

export default Count1

(4)App.js

import { NavLink, Route, Switch, Redirect } from 'react-router-dom'
import Home from './Home'
import Count1 from './Count1'
const App = () => {
  return (
    <>
      <NavLink activeClassName="highlight" to="/home">
        Home
      </NavLink>
      <NavLink activeClassName="highlight" to="/count1">
        Count1
      </NavLink>
      <Switch>
        <Route path="/home" component={Home} />
        <Route path="/count1" component={Count1} />
        <Redirect to="/home" /> {/* Redirect要写在后面 */}
      </Switch>
    </>
  )
}

export default App

3、useEffect的第二个参数

  1. useEffect第二个参数不传,则表示监听所有的state,也就是说name的值改变了也会触发count的打印,此时useEffect相当于componentDidMountcomponentDidUpdate两个钩子。这种情况几乎不用
  2. 如果只需要监听count,只需要传入第二个参数[count],此时useEffect相当于componentDidUpdate
  3. 如果传入[],此时useEffect相当于componentDidMount。若此时useEffect中有return一个函数,那么这个函数相当于componentWillUnmount

4、useEffect第一个参数前不能加async

这种async/await的方式在开发中可以说很常见了

  useEffect(async () => {
    const res = await listApi()
  }, [])

但useEffect中不允许给第一个参数加async,async会导致函数的返回值是一个promise,这样写会报错

react为什么这么设计呢?

  1. useEffect的返回值是在组件卸载前调用的(类似componentWillUnmount)
  2. useEffect可能有个隐藏逻辑:第二次触发useEffect的回调前,前一次触发的行为都已经执行完成,返回的清理函数也执行完成,这样设计逻辑才清楚。如果回调函数是异步的,情况会变得复杂起来,很容易出现bug

推荐的写法:

  const getList = async () => {
    const res = await listApi()
  }
  useEffect(() => {
    getList()
  }, [])

四、useContext

1、context

  1. context的作用是让它所包含的组件树可以共享数据,实现【祖组件】和【后代组件】之间的通信,类似vue中的provide/inject
  2. context,和react-redux中的Provider组件很像,【祖组件】和【后代组件】之间的关系是【提供】和【消费】的关系
       ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.querySelector('#root')
    )

2、如何使用context

①Themes/context.js

import { createContext } from 'react'

export const themes = {
  primary: { color: '#fff', backgroundColor: 'skyblue' },
  danger: { color: '#fff', backgroundColor: 'red' }
}

export const themesContext = createContext(themes)

②Themes/index.js

import { useState, useContext } from 'react'
import { themes, themesContext } from './context'

const Parent = () => {
  const [type, setType] = useState('primary')
  return (
    <>
      <button onClick={() => setType(type === 'primary' ? 'danger' : 'primary')}>
        设置为 {type === 'primary' ? 'danger' : 'primary'} 按钮
      </button>
      <hr />
      {/* 1、被themesContext.Provider包裹的组件,通过value属性传递值,该组件和其后代组件都具有context */}
      <themesContext.Provider value={themes[type]}>
        <Child1 />
      </themesContext.Provider>
    </>
  )
}

const Child1 = () => {
  // 2、通过 useContext hook 可以接收context中的值
  const value = useContext(themesContext)
  return (
    <>
      <button style={value}>按钮</button>
      <GrandSon />
    </>
  )
}

const GrandSon = () => {
  return (
    <>
      {/* 3、通过themesContext.Consumer组件也可以接收context中的值 */}
      <themesContext.Consumer>
        {(value) => <button style={value}>按钮</button>}
      </themesContext.Consumer>
    </>
  )
}

export default Parent

  1. 定义:被CountContext.Provider包裹的组件,通过value属性传递值,该组件和其后代组件都具有context
  2. 接收:通过 useContext hook 可以接收context中的值;如果不用hooks通过CountContext.Consumer组件也可以接收context中的值

3、类式组件中如何接收context

class Child2 extends Component {
  static contextType = themesContext
  render() {
    return (
      <>
        {/* 类式组件第一种方式接收context(需要声明static contextType = themesContext): */}
        <button style={this.context}>按钮</button>
        {/* 类式组件第二种方式接收context */}
        <themesContext.Consumer>
          {(value) => <button style={value}>按钮</button>}
        </themesContext.Consumer>
      </>
    )
  }
}

五、useReducer

1、reducer是什么

reducer是一个纯函数,该函数接收两个参数,一个是状态,一个是用来控制业务逻辑的判断参数

纯函数的特点:

  1. 不得改写参数数据
  2. 不会产生任何副作用,例如:网络请求、输入和输出设备
  3. 不能调用Date.now()或Math.rendom()等不纯的方法
  const initState = { count: 0 }

  const reducer = (state, action) => {
    switch (action.type) {
      case 'add':
        return { count: ++state.count }
      case 'sub':
        return { count: --state.count }
      case 'reset':
      default:
        return initState
    }
  }

2、useReducer的使用

import { useReducer } from 'react'

const ReducerDemo = () => {
  const initState = { count: 0 }

  const reducer = (state, action) => {
    switch (action.type) {
      case 'add':
        return { count: ++state.count }
      case 'sub':
        return { count: --state.count }
      case 'reset':
      default:
        return initState
    }
  }

  const [state, dispatch] = useReducer(reducer, initState)

  return (
    <>
      <h2>当前值:{state.count}</h2>
      <button onClick={() => dispatch({ type: 'add' })}>+</button>
      <button onClick={() => dispatch({ type: 'sub' })}>+</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
    </>
  )
}

export default ReducerDemo

六、useMemo

useMemo主要用来解决使用react hooks产生的无用渲染的性能问题。函数式组件失去了shouldComponentUpdate钩子,也就是说没有办法在组件更新前通过条件判断决定组件是否更新。而且在函数式组件中,没有mountupdate的区别,这意味着函数式组件的每一次调用都会执行内部的所有逻辑,带来了非常大的性能损耗。useMemouseCallback都是解决上述问题的。

1、回顾类式组件中如何解决render无效调用

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

const Test = () => {
  const [count, setCount] = useState(0)

  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Child />
    </>
  )
}

class Child extends Component {
  render() {
    console.log('Child render执行')
    return <div>Child组件</div>
  }
}

export default Test

这段代码会有个问题:类组件继承于Component组件,当父组件(Test)中的state(count)改变时,子组件(Child)会跟着重新render,这种render是无效的

动图.gif

如何优化: (1)第一种方案:使用shouldComponentUpdate钩子

  // 重写shouldComponentUpdate钩子,比较新旧state或props,有变化返回true,没有返回false。这样写的好处:this.setState({})将不会触发render
  shouldComponentUpdate(nextProps, nextState) {
    // 该钩子是在render前调用的,但是可以拿到render后的props和state,所以参数名叫nextProps、nextState
    return this.state.count !== nextState.count
  }

缺点:当stateprops是多个时,需要写的判断太多了

(2)第二种方案:使用PureComponent钩子替代Component(推荐),PureComponent内部重写了shouldComponentUpdate钩子,只有当state或props发生变化时才调用render函数

class Child extends PureComponent {
  render() {
    console.log('Child render执行')
    return <div>Child组件</div>
  }
}

优化后效果:

动图.gif

2、函数式组件中如何优化state更新导致的子组件无效渲染

函数式组件中也会有Component的那个问题,父组件中的某个state更新,会导致子组件重新渲染

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

const Test = () => {
  const [count, setCount] = useState(0)

  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Child />
    </>
  )
}

const Child = () => {
  console.log('Child render执行')
  return <div>Child组件</div>
}

export default Test

可以使用memo函数将子组件包裹起来,memo和PureComponent的作用是一样的

const Child = memo(() => {
  console.log('Child render执行')
  return <div>Child组件</div>
})

1、性能问题展示demo:点击按钮a改变a的时间戳,点击按钮b改变b的时间戳

index.js

import { useState } from 'react'
import Child from './Child'

const Memo = () => {
  const [a, setA] = useState('0')
  const [b, setB] = useState('0')
  return (
    <>
      <button onClick={() => setA(Date.now())}>改变a</button>
      <button onClick={() => setB(Date.now())}>改变b</button>
      <Child a={a} b={b} />
    </>
  )
}

export default Memo

Child.js

const Child = ({ a, b }) => {
  const changeA = (a) => {
    console.log('a被改变')
    return `a的时间戳:${a}`
  }

  const changeB = (b) => {
    console.log('b被改变')
    return `b的时间戳:${b}`
  }

  const currentA = changeA(a)
  const currentB = changeB(b)
  return (
    <>
      <p>{currentA}</p>
      <p>{currentB}</p>
    </>
  )
}

export default Child

动图.gif

当我在改变a的时间戳的时候,请问我改变b了吗?但是这里的changeB就会被调用,这种调用毫无意义

2、使用useMemo优化

Child.js

import { useMemo } from 'react'

const Child = ({ a, b }) => {
  const changeA = (a) => {
    console.log('a被改变')
    return `a的时间戳:${a}`
  }

  const changeB = (b) => {
    console.log('b被改变')
    return `b的时间戳:${b}`
  }

  // const currentA = changeA(a)
  // const currentB = changeB(b)
  const currentA = useMemo(() => changeA(a), [a])
  const currentB = useMemo(() => changeB(b), [b])
  return (
    <>
      <p>{currentA}</p>
      <p>{currentB}</p>
    </>
  )
}

export default Child

动图.gif

此时,点击按钮a只会触发changeA,点击按钮b只会触发changeB

3、useMemo实现computed的效果

import { useState, useMemo } from 'react'

const Memo = () => {
  const [a, setA] = useState(0)
  const [b, setB] = useState(0)
  const [d, setD] = useState(0)

  const add = (type) => {
    switch (type) {
      case 'a':
        setA(a + 1)
        break
      case 'b':
        setB(b + 1)
        break
      case 'd':
        setD(d + 1)
        break
      default:
        return false
    }
  }

  const c = useMemo(() => {
    console.log('useMemo => a或b发生变化了触发')
    return a + b
  }, [a, b])

  return (
    <>
      <p>a:{a}</p>
      <p>b:{b}</p>
      <p>c:{c}</p>
      <p>d:{d}</p>
      <button onClick={() => add('a')}>+a</button>
      <button onClick={() => add('b')}>+b</button>
      <button onClick={() => add('d')}>+d</button>
    </>
  )
}

export default Memo

动图.gif 此时,a和b发生变化会触发useMemo中的函数,c的值为a+b;d的变化不会触发useMemo中的函数

另外,useMemo中的函数可以返回DOM:

  const c = useMemo(() => {
    console.log('useMemo => a或b发生变化了触发')
    return <h1>{a + b}</h1>
  }, [a, b])

4、useMemo和useCallback的区别

  1. useMemo传入的函数需要有返回值,useMemo的返回值是一个具体的值,useCallback的返回值是一个函数
  2. useMemo只能声明在函数式组件内部,而React.memo是将子组件包裹起来
  3. 使用useCallback的场景:事件的回调、处理一些逻辑的函数。使用useMemo的场景:根据已有状态计算额外的数据,计算的过程中很消耗性能
  4. useCallback(fn, deps)相当于useMemo(() => fn, deps)
import React, { useState, memo, useCallback, useMemo } from 'react'

const Test = () => {
  const [range, setRange] = useState({ max: 10 })
  const [count, setCount] = useState(0)

  const render = useMemo(() => {
    console.log('调用我了')
    const list = []
    for (let i = 0; i < range.max; i++) {
      list.push(<li key={i}>{i}</li>)
    }
    return list
  }, [range])

  // const render = useCallback(() => {
  //   console.log('调用我了')
  //   const list = []
  //   for (let i = 0; i < range.max; i++) {
  //     list.push(<li key={i}>{i}</li>)
  //   }
  //   return list
  // }, [range])

  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setRange({ max: range.max + 1 })}>
        change range
      </button>
      <List render={render}></List>
    </>
  )
}

const List = memo((props) => {
  console.log('子组件被调用')
  return <ul>{props.render}</ul>
  return <ul>{props.render()}</ul>
})

export default Test

七、useCallback

1、基本写法

  const memoizedCallback = useCallback(() => {
    doSomething(a, b)
  }, [a, b])

2、为什么要使用useCallback

  • 在函数式组件中,定义在组件内的函数会随着状态值的更新而重新渲染,函数 中定义的函数会被频繁定义。
  • 父子组件通信时,会在父组件中定义修改状态的方法传入到子组件中,子组件中接收到父组件的函数被频繁更新,会造成父组件状态变更时子组件重新渲染。
  • 使用useCallback结合memo可以有效的减少子组件更新频率,提高效率
  • 父组件传递到子组件中的函数,在父组件中定义该函数时要通过useCallback缓存起来

未使用useCallback和使用useCallback的区别:

①count值的改变会造成aa函数的重新定义,CallBack中的自组件也会被重新渲染

import React, { useState } from 'react'
const CallBack = () => {
  const aa = () => {
    console.log('aa')
  }
  aa()

  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <Child />
    </>
  )
}

const Child = () => {
  console.log('子组件渲染')
  return <>Child</>
}

export default CallBack

动图.gif

②使用memo将子组件包裹,子组件的props发生变化时才会重新渲染

    const Child = React.memo(() => {
      console.log('子组件渲染')
      return <>Child</>
    })

动图.gif

③当Child组件传入props时,由于aa函数被重新定义,props会更新,还是会造成子组件的重新渲染

      <Child aa={aa} />

    const Child = React.memo((props) => {
      console.log('子组件渲染', props)
      return <>Child</>
    })

动图.gif

④使用useCallback和memo结合

import React, { useState, useCallback } from 'react'
const CallBack = () => {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('小明')

  // 当count发生变化时,aa函数才会重新定义;name的改变不会引发aa的重新定义,也就不会造成Child组件的重新渲染
  const aa = useCallback(() => {
    console.log('aa')
  }, [count])
  aa()

  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <p>{name}</p>
      <button onClick={() => setName(name === '小明' ? '大明' : '小明')}>
        changeName
      </button>
      <Child aa={aa} />
    </>
  )
}

const Child = React.memo((props) => {
  console.log('子组件渲染', props)
  return <>Child</>
})

export default CallBack

动图.gif

⑤useCallback不依赖任何的state,又有memo的加持,此时count和name的变化都不会造成Child重新渲染

  // aa函数只会在CallBack组件初始化时定义,当count和name值发生变化时不会被重新定义
  const aa = useCallback(() => {
    console.log('aa')
  }, []) // 这里没有依赖的值,useCallback返回的是“记忆函数”,也就是上一次定义的函数,那么aa就没有发生变化,那么Child的props就没有发生变化,Child组件被memo缓存,就不会重新渲染
  aa()

动图.gif

八、useRef

1、获取DOM,操作DOM

import { useState, useRef } from 'react'

const Ref = () => {
  const [value, setValue] = useState('初始值')
  const inputRef = useRef(null)

  return (
    <>
      {/* 1、useState和value/change结合进行赋值/取值 */}
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        type="text"
      />
      <button onClick={() => setValue('给你个大逼斗')}>点击赋值</button>
      <button onClick={() => console.log(value)}>点击取值</button>
      <hr />
      {/* 2、使用useRef进行赋值/取值 */}
      <input ref={inputRef} defaultValue="100" type="text" />
      <button onClick={() => (inputRef.current.value = 'hello hooks')}>
        点击赋值
      </button>
      <button onClick={() => console.log(inputRef.current.value)}>
        点击取值
      </button>
    </>
  )
}

export default Ref

2、forwardRef

用于向组件中传递ref

const FInput = React.forwardRef((props, ref) => {
  return <input type="text" {...props} ref={ref} />
})

const Home = () => {
  const inputRef = useRef()
  useEffect(() => {
    inputRef.current.focus()
    inputRef.current.value = 'hello'
  }, [])
  return (
    <>
      <FInput placeholder="请输入" ref={inputRef} />
    </>
  )
}

九、自定义hook函数获取窗口大小

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

// 自定义hook,需要以use开头
const useWinSize = () => {
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  })
  const onResize = useCallback(() => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    })
  }, [])
  useEffect(() => {
    window.addEventListener('resize', onResize)
    return () => {
      window.removeEventListener('resize', onResize)
    }
  }, [])
  return size
}

const Size = () => {
  const size = useWinSize()
  return <>窗口的大小:{`${size.width} * ${size.height}`}</>
}

export default Size

九、