react-hooks进阶,理清思路,解决问题,如何清除副作用

1,720 阅读13分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

  • useState 回调函数形式的参数
  • useEffect 清理副作用
  • useRef操作DOM
  • useContext组件通讯

useEffect发送请求

目的

能够在函数组件中通过useEffect发送ajax请求

格式

// 正确使用
useEffect(() => {
  // 定义一个函数
  async function fetchMyAPI() {
    let url = 'http://something/' + productId
    let config = {}
    const response = await myFetch(url)
  }
  // 调用它
  fetchMyAPI()
}, [productId])

注意:effect 只能是一个同步函数,不能使用 async

// 错误演示:// 不要给 effect 添加 async
useEffect(async () => {
    const res = awiat xxx()
    return ()=> {
        
    }
}, [])

useEffect-副作用函数的返回值-清理副作用

目标

掌握副作用函数的返回值的使用,能在在组件卸载的时候,清除注册的事件

副作用函数的返回值

格式

useEffect(() => {
  // 副作用函数的内容
​
  return 副作用函数的返回值
}, [])

副作用函数的返回值是可选的,可省略。也可以返回一个清理函数,用来清理副作用

useEffect(() => {
  // 副作用函数的内容
​
  return ()=>{ /* 做清理工作*/ } // 清理函数
}, [])

清理函数的执行时机:

  • 清理函数在组件卸载时以及下一次副作用函数调用之前执行

示例:执行时机

import React, { useEffect, useState } from 'react'
import ReactDom from 'react-dom'function Com1 () {
  useEffect(() => {
    console.log('子组件的副作用函数')
    
    return ()=>{
      console.log('子组件的副作用函数的清理函数')
    }
  }, [])
  return <div>子组件</div>
}
export default function App () {
  const [isShow, setIsShow] = useState(true)
  const [count, setCcount] = useState(0)
  
  useEffect(() => {
    console.log('父组件的副作用函数的内容')
​
    return () => {
      
      console.log('父组件的副作用函数的 清理函数 的内容')
    }
  }, [count])
  
  return (
    <div>
      {isShow && <Com1 />}
      <button onClick={() => setIsShow(!isShow)}>删除|创建子组件</button>
        <button onClick={() => setCcount(count+1)}>点击+1, {count}</button>
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))
​

示例-模拟componentWillUnmount

useEffect(()=>{
  return ()=>{
    // 做清理工作
  }
}, [])

小结

情况1:不带第二个参数。执行时机:初始执行,每次更新之后都要执行。

模拟 componentDidmount + componentDidUpdate

useEffect(() => {
  // 副作用函数的内容
})

情况2:带第二个参数,参数是空数组。执行时机:只执行第一次。

模拟 componentDidMount

useEffect(() => {
  // 副作用函数的内容
}, [])

使用场景:1 事件绑定 2 发送请求获取数据 等。

情况3:带第二个参数(数组格式),并指定了依赖项。执行时机:(1)初始执行一次 (2)依赖项的值变化了,执行一次

useEffect(() => {
  // 副作用函数的内容
}, [依赖项1,依赖项2,....])

这里的依赖项就是组件中定义的状态。

情况4:依赖项为空,没有具体的副作用函数,有副作用函数的清理函数

模拟:componentWillUnMount

useEffect(() => {
  
  return () => {
    // 副作用函数的清理函数,模拟 componentWillUnMount
  }
  
}, [])

useEffect-清理副作用-案例

目标

掌握清理组件中定义的事件

问题导入

删除子组件之后,事件还有么?

import React, { useEffect, useState } from 'react'
import ReactDom from 'react-dom'function Com1 () {
  useEffect(() => {
    window.addEventListener('mousemove', (e) => {
      console.log(e.pageX, e.pageY)
    })
  }, [])
  return <div>子组件</div>
}
export default function App () {
  const [isShow, setIsShow] = useState(true)
  return (
    <div>
      {isShow && <Com1 />}
      <button onClick={() => setIsShow(!isShow)}>切换</button>
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))
​

子组件被删除了,但是,事件处理还在,所以要清理副作用。

解决

import React, { useEffect, useState } from 'react'
import ReactDom from 'react-dom'function Com1 () {
  useEffect(() => {
    const fn = (e) => {
      console.log(e.pageX, e.pageY)
    }
+    window.addEventListener('mousemove', fn)
​
+    return () => {
      window.removeEventListener('mousemove', fn)
      console.log('组件删除了')
    }
  }, [])
  return <div>子组件</div>
}
export default function App () {
  const [isShow, setIsShow] = useState(true)
  return (
    <div>
      {isShow && <Com1 />}
      <button onClick={() => setIsShow(!isShow)}>切换</button>
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))
​

案例-鼠标跟随

目标

能够实现案例,让图片跟随鼠标移动

思路

  • 通过useState提供状态
  • 通过useEffect给document注册鼠标移动事件
  • 在组件销毁的时候清理副作用
import { useEffect, useState } from 'react'
import img from './1.gif'
export default function Move() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  })
  useEffect(() => {
    const move = (e) => {
      console.log('移动')
​
      setPosition({
        x: e.pageX,
        y: e.pageY
      })
    }
    document.addEventListener('mousemove', move)
    console.log('注册事件')
    return function () {
      document.removeEventListener('mousemove', move)
    }
  }, [])
  return (
    <div>
      <img
        src={img}
        style={{
          position: 'absolute',
          top: position.y + 1,
          left: position.x + 1
        }}
        alt=""
      />
    </div>
  )
}
​

自定义hooks-应用场景

目标

能够理解逻辑复用的含义

导入

目前还有另一功能,要跟踪鼠标的位置,并在页面上输出。

仿写代码如下:

import { useEffect, useState } from 'react'
import ReactDom from 'react-dom'
export default function App () {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  })
  useEffect(() => {
    const move = (e) => {
      console.log('移动')
​
      setPosition({
        x: e.pageX,
        y: e.pageY
      })
    }
    document.addEventListener('mousemove', move)
    console.log('注册事件')
    return function () {
      document.removeEventListener('mousemove', move)
    }
  }, [])
  return (
    <div>
      {position.x}, {position.y}
    </div>
  )
}
​
ReactDom.render(<App />, document.getElementById('root'))

思考

它与案例-鼠标跟随中的代码能否重用?

自定义hooks-实操

定义

除了使用内置的 Hooks 之外,还可以创建自己的 Hooks(自定义 Hooks)。

自定义 Hooks 针对不同组件实现不同状态逻辑复用

  • 自定义 Hooks 是一个函数,约定函数名称必须以 use 开头,React 就是通过函数名称是否以 use 开头来判断是不是 Hooks
  • Hooks 只能在函数组件中或其他自定义 Hooks 中使用,否则,会报错!
  • 自定义 Hooks 用来提取组件的状态逻辑,根据不同功能可以有不同的参数和返回值(就像使用普通函数一样)

使用场景

将组件状态逻辑提取到可重用的函数(自定义 Hooks)中,实现状态逻辑复用。

核心代码

// museMouse.js
import { useEffect, useState } from 'react'
export default function useMouse() {
  // 逻辑处理
  const [position, setPosition] = useState({
    x: 0,
    y: 0,
  })
​
  useEffect(() => {
    const move = (e) => {
      setPosition({
        x: e.pageX,
        y: e.pageY,
      })
    }
    document.addEventListener('mousemove', move)
    return () => {
      document.removeEventListener('mousemove', move)
    }
  }, [])
  
  // 返回状态
  return position
}
​

小结

  1. 自定义hooks是一个****
  2. 自定义hooks的名字必须以****开头
  3. 自定义hooks只能在中使用

useRef-操作DOM

目标

能够使用useRef操作DOM

内容

使用场景:在 React 中进行 DOM 操作时,用来获取 DOM

作用:返回一个带有 current 属性的可变对象,通过该对象就可以进行 DOM 操作了。

const inputRef = useRef(null)

解释:

  • 参数:在获取 DOM 时,一般都设置为 null
  • 返回值:包含 current 属性的对象。
  • 注意:只要在 React 中进行 DOM 操作,都可以通过 useRef Hook 来获取 DOM(比如,获取 DOM 的宽高等)。
  • 注意:useRef不仅仅可以用于操作DOM,还可以操作组件

核心代码:

// 1. 导入 useRef
import React, { useRef } from 'react'
import ReactDom from 'react-dom'class Com1 extends React.Component {
  state = {
    a: 100
  }
​
  render () {
    return <div>com1, {this.state.a}</div>
  }
}
​
export default function App () {
  // 2. 调用useRef(初值),得到引用对象
  // 3. 把引用对象设置ref 给任意的组件/元素
  // 4. 通过引用对象.current 获取 组件/元素
  const refTxt = useRef(null)
  const refCom1 = useRef(null)
  console.log(refTxt)
​
  const click = () => {
    console.log(refTxt, refCom1)
    console.log(refCom1.current.state.a)
    // console.log(refTxt.current.value)
    // refTxt.current.style.backgroundColor = 'red'
  }
  return (
    <div>
      useRef, <input ref={refTxt} />{' '}
      <button onClick={click}>点击,获取input中的值</button>
      <br />
      <Com1 ref={refCom1} />
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))
​

useRef-在多次渲染之间共享数据-停止定时器

函数组件虽然非常直观,简化了思考 UI 实现的逻辑,但是比起 Class 组件,还缺少了一个很重要的能力:在多次渲染之间共享数据。

目标

学习使用useRef在多次渲染之间共享数据

问题导入

import React, { useEffect, useState } from 'react'
import ReactDom from 'react-dom'
export default function App () {
  const [count, setCount] = useState(0)
  let timeId = null
  console.log(timeId)
  useEffect(() => {
    timeId = setInterval(() => {
      setCount((count) => count + 1)
    }, 1000)
    console.log(timeId)
  }, [])
​
  const hClick = () => {
    console.log(timeId)
    clearInterval(timeId)
  }
​
  return (
    <div>
      count:{count}
      <button onClick={hClick}>点击停止定时器</button>
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))
​

分析

setCount会导致组件重新渲染,而重新渲染时,会重复执行如下代码

let timeId = null

所以,无法保存timeId

思路

将timeId保存在一个多次渲染也不会丢失的位置。

步骤

// 1. 导入 useRef
import React, { useRef } from 'react'
​
组件() {
  // 2. 调用useRef,
  const timeRef = useRef(null)
  // 3. 向引用的current中存入数据
  timeRef.current = 你需要在多次渲染之间共享的数据
}

解决

import React, { useEffect, useRef, useState } from 'react'
import ReactDom from 'react-dom'
export default function App () {
  const [count, setCount] = useState(0)
  const timeRef = useRef(null)
  console.log(timeRef.current)
  useEffect(() => {
    timeRef.current = setInterval(() => {
      setCount((count) => count + 1)
    }, 1000)
    console.log(timeRef.current)
  }, [])
​
  const hClick = () => {
    console.log(timeRef.current)
    clearInterval(timeRef.current)
  }
​
  return (
    <div>
      count:{count}
      <button onClick={hClick}>点击停止定时器</button>
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))

自定义hooks-封装倒计时-需求-验证码

目标

写代码完成效果

描述:点击按钮:按钮不可用,开始倒计时10秒,10秒结束之后,按钮恢复可用

思路

  1. 定义两个state:

    1. canSend: 初始为true,倒计时期间为false,绑定给按钮的 disabled属性
    2. count: 初始为0,点击按钮之后count置为60,绑定给文字
  2. 点击按钮之后:

    1. canSend改成false
    2. 开启定时器setInterval

基本实现

import React, { useState, useEffect, useRef } from 'react'import ReactDom from 'react-dom'export default function App () {
  // 多次渲染之间共享数据:定时器
  const timeId = useRef(null)
  
  const [canSend, setCanSend] = useState(true)
  const [count, setCount] = useState(initCount)
​
  const send = () => {
    setCanSend(false)
    setCount(10)
    timeId.current = setInterval(() => {
      setCount((count) => count - 1)
    }, 1000)
  }
  
  useEffect(() => {
    return () => {
      console.log('..........')
      clearInterval(timeId.current)
    }
  }, [])
​
  useEffect(
    () => {
      console.log(count, timeId.current)
      if (count === 0) {
        clearInterval(timeId.current)
        // 倒计时结束
        // 恢复按钮可用
        setCanSend(true)
      }
    },
    [count]
  )
​
  return (
    <div>
      <button disabled={!canSend} onClick={send}>
        {canSend ? '发送验证码' : count + '秒之后再发送'}
      </button>
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))
​

自定义hooks-封装倒计时-需求-404页面3秒跳转

/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect, useRef } from 'react'
import ReactDom from 'react-dom'
export default function App () {
  const [count, setCount] = useState(5)
  const refTimerId = useRef(null)
​
  useEffect(() => {
    // 1. 开始倒数
    setCount(5)
    // 2. 启动
    refTimerId.current = setInterval(() => {
      setCount((count) => count - 1)
    }, 1000)
  }, [])
​
  useEffect(
    () => {
      console.log('观察到count变了', count)
      if (count === 0) {
        //    倒数结束,要:
        // 1. 跳转
        location.href = 'http://www.baidu.com'
        console.log('location.href = http://www.baidu.com')
        // 2.清空定时器
        clearInterval(refTimerId.current)
      }
    },
    [count]
  )
​
  return (
    <div>
      你要的页面不存在,{count}秒之后,自动跳到主页。或者点击<a href="http://www.baidu.com">这里</a>跳转
    </div>
  )
}
​
ReactDom.render(<App />, document.getElementById('root'))
​

自定义hooks-封装倒计时-提炼hooks

export default function useCountDown (initCount = 10, callBack = () => {}) {
  const timeId = useRef(null)
​
  const [count, setCount] = useState(initCount)
​
  const start = () => {
    setCount(initCount)
    timeId.current = setInterval(() => {
      setCount((count) => count - 1)
    }, 1000)
  }
  useEffect(() => {
    return () => {
      console.log('..........')
      clearInterval(timeId.current)
    }
  }, [])
​
  useEffect(
    () => {
      console.log(count, timeId.current)
      if (count === 0) {
        clearInterval(timeId.current)
        callBack()
      }
    },
    [count]
  )
​
  return { count, start }
}

完整功能

import React, { useState, useEffect, useRef } from 'react'import ReactDom from 'react-dom'import useCountDown from './useCountDown' // 引入hooksexport default function App () {
  const { count, start } = useCountDown(10, () => {
    setCanSend(true)
  })
  const [canSend, setCanSend] = useState(true)
​
  const send = () => {
    setCanSend(false)
    start(60)
  }
  return (
    <div>
      <button disabled={!canSend} onClick={send}>
        {canSend ? '发送验证码' : count + '秒之后再发送'}
      </button>
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))
​

useContext-全局状态

目标

掌握useContext的使用

目标

基础素材

目录

- index.js   # 根组件,包含father  
- father.js  # 父组件,包含son
- son.js     # 子组件

index.js

import React, { useState, useContext } from 'react'
import Father from './father'export default function App() {
    const [color, setColor] = useState('red')
    return (
        <div style={{border:'1px solid #ccc', margin:10}}>
            根组件:color:{color}
            <button onClick={()=>{setColor('green')}}>改成 green</button>
​
            <Father />
        </div>
    )
}

father.js

import Son from './son.js'
const Father = () => {
    return (
    <div style={{border:'1px solid #ccc', margin:10}}>
        父组件
        <Son/>
     </div>)
}
​
export default Father

son.js

const Son = () => {
    return (
        <div style={{border:'1px solid #ccc', margin:10}}>
        子组件: color: {'red'}</div>)
}
​
export default Son

useContext步骤

共3步:

  1. 导入并调用createContext方法,得到Context对象,导出

    import { createContext } from 'react'
    export const Context = createContext()
    
  2. 使用 Provider 组件包裹根组件,并通过 value 属性提供要共享的数据

    return (
      <Context.Provider value={ 这里放要传递的数据 }>
        <根组件的内容/>
      </Provider>
    )
    
  3. 在任意后代组件中,如果希望获取公共数据:

    导入useContext;调用useContext(第一步中导出的context) 得到value的值

    import React, { useContext } from 'react'
    import { Context } from './index'
    const 函数组件 = () => {
        const 公共数据 = useContext(Context)
        return ( 函数组件的内容 )
    }
    

完成目标

index.js

import React, { useState, useContext } from 'react'
import Father from './father'
export const Context = React.createContext()   //  这里有导出export default function App() {
    const [color, setColor] = useState('red')
    return (
        <Context.Provider value={color}>
        <div style={{border:'1px solid #ccc', margin:10}}>
            根组件:color:{color}
            <button onClick={()=>{setColor('green')}}>改成 green</button>
​
            <Father />
        </div>
        </Context.Provider>
    )
}

father.js

import Son from './son.js'
const Father = () => {
    return (
    <div style={{border:'1px solid #ccc', margin:10}}>
        父组件
        <Son/>
        </div>)
}
​
export default Father

son.js

import React, { useContext } from 'react'
import { Context } from './index'
const Son = () => {
    const color = useContext(Context)
    return (
        <div style={{border:'1px solid #ccc', margin:10}}>
        子组件: color: {color}</div>)
}
​
export default Son

购物车案例-发送请求-获取列表数据

目标

发送请求,获取到购物车数据

步骤

  1. 安装axios
  2. 使用useState hooks提供状态
  3. 使用useEffect发送请求获取数据

核心代码

  • 安装axios
yarn add axios
  • 发送请求,获取数据
useEffect(() => {
  // 判断本地是否有数据
  const arr = JSON.parse(localStorage.getItem('list')) || []
  if (arr.length) {
    return setList(arr)
  }
  // 本地没有数据,发送请求,获取数据
  const getList = async () => {
    const res = await axios.get('https://www.escook.cn/api/cart')
    setList(res.data.list)
  }
  getList()
}, [])

MyCount组件的封装-基本结构

  • 基本结构
import React from 'react'
import './index.scss'
export default function MyCount() {
  return (
    <div className="my-counter">
      <button type="button" className="btn btn-light">
        -
      </button>
      <input type="number" className="form-control inp" value="1" />
      <button type="button" className="btn btn-light">
        +
      </button>
    </div>
  )
}
​
  • 样式
.my-counter {
  display: flex;
  .inp {
    width: 45px;
    text-align: center;
    margin: 0 10px;
  }
}
​
  • 在GoodsItem组件中渲染
import MyCount from '../MyCount'
​
<MyCount></MyCount>

数量控制-useContext

目标

使用useContext优化组件的通讯,完成数量的修改

步骤

  1. 在App.js中创建Context对象,并且导出
export const Context = createContext()
  1. 在App.js中,通过Provider提供方法 changeCount
import React, { useState, useEffect, createContext } from 'react'
import ReactDom from 'react-dom'
import GoodsItem from './components/GoodsItem/GoodsItem'
import MyFooter from './components/MyFooter/MyFooter'
import MyHeader from './components/MyHeader/MyHeader'
import 'bootstrap/dist/css/bootstrap.css'
import './app.scss'
import axios from 'axios'export const Context = createContext()
// const initList = [
//   {
//     id: 1,
//     goods_name: '班俏BANQIAO超火ins潮卫衣女士2020秋季新款韩版宽松慵懒风薄款外套带帽上衣',
//     goods_img: 'https://www.escook.cn/vuebase/pics/1.png',
//     goods_price: 108,
//     goods_count: 1,
//     goods_state: true
//   },
//   {
//     id: 2,
//     goods_name: '嘉叶希连帽卫衣女春秋薄款2020新款宽松bf韩版字母印花中长款外套ins潮',
//     goods_img: 'https://www.escook.cn/vuebase/pics/2.png',
//     goods_price: 129,
//     goods_count: 1,
//     goods_state: true
//   },
//   {
//     id: 3,
//     goods_name: '思蜜怡2020休闲运动套装女春秋季新款时尚大码宽松长袖卫衣两件套',
//     goods_img: 'https://www.escook.cn/vuebase/pics/3.png',
//     goods_price: 198,
//     goods_count: 1,
//     goods_state: false
//   },
//   {
//     id: 4,
//     goods_name: '思蜜怡卫衣女加绒加厚2020秋冬装新款韩版宽松上衣连帽中长款外套',
//     goods_img: 'https://www.escook.cn/vuebase/pics/4.png',
//     goods_price: 99,
//     goods_count: 1,
//     goods_state: false
//   },
//   {
//     id: 5,
//     goods_name: '幂凝早秋季卫衣女春秋装韩版宽松中长款假两件上衣薄款ins盐系外套潮',
//     goods_img: 'https://www.escook.cn/vuebase/pics/5.png',
//     goods_price: 156,
//     goods_count: 1,
//     goods_state: true
//   },
//   {
//     id: 6,
//     goods_name: 'ME&CITY女装冬季新款针织抽绳休闲连帽卫衣女',
//     goods_img: 'https://www.escook.cn/vuebase/pics/6.png',
//     goods_price: 142.8,
//     goods_count: 1,
//     goods_state: true
//   },
//   {
//     id: 7,
//     goods_name: '幂凝假两件女士卫衣秋冬女装2020年新款韩版宽松春秋季薄款ins潮外套',
//     goods_img: 'https://www.escook.cn/vuebase/pics/7.png',
//     goods_price: 219,
//     goods_count: 2,
//     goods_state: true
//   },
//   {
//     id: 8,
//     goods_name: '依魅人2020休闲运动衣套装女秋季新款秋季韩版宽松卫衣 时尚两件套',
//     goods_img: 'https://www.escook.cn/vuebase/pics/8.png',
//     goods_price: 178,
//     goods_count: 1,
//     goods_state: true
//   },
//   {
//     id: 9,
//     goods_name: '芷臻(zhizhen)加厚卫衣2020春秋季女长袖韩版宽松短款加绒春秋装连帽开衫外套冬',
//     goods_img: 'https://www.escook.cn/vuebase/pics/9.png',
//     goods_price: 128,
//     goods_count: 1,
//     goods_state: false
//   },
//   {
//     id: 10,
//     goods_name: 'Semir森马卫衣女冬装2019新款可爱甜美大撞色小清新连帽薄绒女士套头衫',
//     goods_img: 'https://www.escook.cn/vuebase/pics/10.png',
//     goods_price: 153,
//     goods_count: 1,
//     goods_state: false
//   }
// ]export default function App () {
  console.log('渲染APP....')
​
  const [list, setList] = useState([])
  //   const [list, setList] = useState(
  //     JSON.parse(localStorage.getItem('list')) || initList
  //   )
​
  //   console.log(list, setList)
​
  useEffect(
    () => {
      console.log('保存list')
      localStorage.setItem('list', JSON.stringify(list))
    },
    [list]
  )
​
  useEffect(() => {
    // 判断本地是否有数据
    const arr = JSON.parse(localStorage.getItem('list')) || []
    if (arr.length) {
      return setList(arr)
    }
    // 本地没有数据,发送请求,获取数据
    const getList = async () => {
      const res = await axios.get('https://www.escook.cn/api/cart')
      setList(res.data.list)
    }
    getList()
  }, [])
​
  // 找到id为 id的商品,设置它的状态为 newState
  const setGoodSelectState = (id, newState) => {
    console.log(id, newState) // 1 false
    const newList = list.map((it) => {
      if (it.id === id) {
        return { ...it, goods_state: newState }
      } else {
        return { ...it }
      }
    })
    setList(newList)
  }
​
  // 找到id为 id的商品,设置它的状态为 newState
  const setGoodCount = (id, newCount) => {
    console.log(id, newCount) // 1 false
    const newList = list.map((it) => {
      if (it.id === id) {
        return { ...it, goods_count: newCount }
      } else {
        return { ...it }
      }
    })
    setList(newList)
  }
​
  // 把所有的商品全部改成newState
  const setCheckAll = (newState) => {
    const newList = list.map((it) => {
      return { ...it, goods_state: newState }
    })
    setList(newList)
  }
​
  return (
    <Context.Provider value={{ setGoodCount }}>
      <div className="app">
        <MyHeader />
​
        {list.map((goods) => {
          return (
            <GoodsItem
              setGoodSelectState={setGoodSelectState}
              goods={goods}
              key={goods.id}
            />
          )
        })}
​
        <MyFooter list={list} setCheckAll={setCheckAll} />
      </div>
    </Context.Provider>
  )
}
ReactDom.render(<App />, document.getElementById('root'))
​
  1. 在myCount组件中,使用useContext获取数据
/* eslint-disable react/prop-types */
/* eslint-disable no-unused-vars */
import React, { useContext } from 'react'
import { Context } from '../../App'
import './index.scss'export default function MyCount (props) {
  const { goods } = props
  const { setGoodCount } = useContext(Context)
  return (
    <div className="my-counter">
      <button
        type="button"
        className="btn btn-light"
        onClick={() => setGoodCount(goods.id, goods.goods_count - 1)}>
        -
      </button>
      <input
        type="number"
        className="form-control inp"
        value={goods.goods_count}
      />
      <button
        type="button"
        className="btn btn-light"
        onClick={() => setGoodCount(goods.id, goods.goods_count + 1)}>
        +
      </button>
    </div>
  )
}

拓展-使用 ESLint 插件帮助检查 Hooks 的使用

刚才你已经看到了使用 Hooks 的一些特性和要遵循的规则,那么应用到日常的开发中,就必须时刻注意不能写错。

包括这么三点:

  • 在 useEffect 的回调函数中使用的变量,都必须在依赖项中声明;
  • Hooks 不能出现在条件语句或者循环中,也不能出现在 return 之后;
  • Hooks 只能在函数组件或者自定义 Hooks 中使用。

React 官方为我们提供了一个 ESLint 的插件,专门用来检查 Hooks 是否正确被使用,它就是 eslint-plugin-react-hooks 。通过这个插件,如果发现缺少依赖项定义这样违反规则的情况,就会报一个错误提示(类似于语法错误的提示),方便进行修改,从而避免 Hooks 的错误使用。

使用步骤

  1. 安装这个插件:npm install eslint-plugin-react-hooks -D

  2. 修改:ESLint 配置文件。

    加入两个规则:rules-of-hooks 和 exhaustive-deps。如下:

    {
      "plugins": [
        // ...
        "react-hooks"
      ],
      "rules": {
        // ...
        // 检查 Hooks 的使用规则
        "react-hooks/rules-of-hooks": "error", 
        // 检查依赖项的声明
        "react-hooks/exhaustive-deps": "warn"
      }
    }