react Hooks 进阶之路

1,087 阅读13分钟

Hooks是什么?

Hooks:钩子、钓钩、钩住。 一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

作用: 为函数组件提供状态、生命周期等原本 在Class 组件中才提供的功能

  • Hooks 只能在函数组件中使用
  • 可以理解为通过 Hooks 为函数组件钩入 class 组件的特性

Hooks 前后,组件开发模式的对比

  • React v16.8 以前: class 组件(提供状态) + 函数组件(展示内容)

  • React v16.8 及其以后:

    1. class 组件(提供状态) + 函数组件(展示内容)
    2. Hooks(提供状态) + 函数组件(展示内容)
    3. 混用以上两种方式:部分功能用 class 组件,部分功能用 Hooks+函数组件

为什么要有 Hooks

两个角度:1 组件的状态逻辑复用 2 class 组件自身的问题

react组件的本质

React 是用于构建用户界面的 JavaScript 库 。

React组件是对特定功能的封装,主要用来对UI进行拆分。

React 组件的模型其实很直观,就是从 Model 到 View 的映射,这里的 Model 对应到 React 中就是 state 和 props

image.png

公式:组件(State+Props) = UI

class 组件自身的问题

根据状态来渲染UI这件事上,class 组件并没有发挥它最重要的功能:

  • 组件之间很少继承
  • 组件之间很少相互访问

函数式组件的好处

  1. 函数本身比较简单,更好的胜任根据状态来渲染UI这件事
  2. hooks让函数组件内部有了维护状态的能力
  3. hooks带来了组件的逻辑复用能力

hooks的使用策略

策略

  1. react没有计划从React中移除class

  2. Hook 和现有代码可以同时工作,你可以渐进式地使用:

    1. 不推荐直接使用 Hooks 大规模重构现有组件
    2. 推荐新功能用 Hooks,复杂功能实现不了的,也可以继续用 class
    3. 找一个功能简单、非核心功能的组件开始使用 hooks
  3. class 组件相关的 API 在hooks中可以不用

    1. state与setState
    2. 钩子函数,componentDidMountcomponentDidUpdatecomponentWillUnmount
    3. `this 相关的用法
  4. 原来学习的内容还是要用的

    1. JSX:{}onClick={handleClick}、条件渲染、列表渲染、样式处理等
    2. 组件:函数组件、组件通讯
    3. React 开发理念:单向数据流状态提升

useState-基本使用-认识第一个hooks

能够使用useState为函数组件提供状态

使用场景

当你想要在函数组件中,使用组件状态时,就要使用 useState 这个Hook 了

作用

为函数组件提供状态(state

步骤

  1. 导入 useState 函数

  2. 调用 useState 函数,传入初始值,返回状态修改状态的函数

  3. 使用

    1. 在 JSX 中展示状态
    2. 特定的时机调用修改状态的函数来改状态

你快看

import { useState } from 'react'
//  useState 是hook,hook是use开头的函数
const Count = () => {  
  // 0 是初始值
  // 返回值是一个数组
  const stateArray = useState(0)

  // 状态值 -> 0
  const state = stateArray[0]
  // 修改状态的函数
  const setState = stateArray[1]

  return (
    <div>
      {/* 展示状态值 */}
      <h1>useState Hook -> {state}</h1>
      {/* 点击按钮,让状态值 +1 */}
      <button onClick={() => setState(state + 1)}>+1</button>
    </div>
  )
}
  • 参数:状态初始值。比如,传入 0 表示该状态的初始值为 0

    • 注意:此处的状态可以是任意值(比如,数值、字符串等),而 class 组件中的 state 必须是对象
  • 返回值:数组,包含两个值:1 状态值(state) 2 修改该状态的函数(setState)

使用数组解构简化代码

使用数组解构简化 useState 的使用。约定:修改状态的函数名称以 set 开头,后面跟上状态的名称

// 解构出来的名称可以是任意名称

const [state, setState] = useState(0)
const [age, setAge] = useState(0)
const [count, setCount] = useState(0)

useState-处理表单元素

  1. 用useState初始化内容和修改内容的方法
  2. 向input元素上设置value和onChange属性

落地示例

import React, { useState } from 'react'
import ReactDom from 'react-dom'
export default function App () {
  const [content, setContent] = useState('')
  return (
    <div>
      {content}
      <input value={content} onChange={(e) => setContent(e.target.value)} />
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))

useState回调函数格式

useState 两种格式

格式1:传入值

useState(0) useState('abc')

格式2:传入回调

useState(() => { return 初始值 })

  1. 回调函数的返回值就是状态的当前值
  2. 回调函数只会触发一次

使用场景

格式1:传入值

如果状态就是一个普通的数据(比如,字符串、数字、数组等)都可以直接使用 useState(普通的数据)

格式2:传入回调

  1. 初始状态需要经过一些计算得到。 useState(()=>{这里有一些计算, return 结果}))

setXXX的参数可以是回调

状态需要迭代累计。 setXXXXX((上一次的值) => { return 新值 })

示例-经典场景

希望state有连续叠加的效果

import React, { useState } from 'react'
import ReactDom from 'react-dom'
export default function App () {
  const [count, setCount] = useState(0)
  const hClick1 = () => {
    setCount(count+1)
    setCount(count+1)
    setCount(count+1)
  }
  const hClick2 = () => {
   setCount((count)=>count+1)
   setCount((count)=>count+1)
   setCount((count)=>count+1)
  }
  
  return (
    <div>
      count:{count}
      <button onClick={hClick1}>多次连续setCount-值</button>
      <button onClick={hClick2}>多次连续setCount-值</button>
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))

小结

  1. useState的参数有两种格式

    1. const [count, setCount] = useState(0)
    2. 回调函数 const [count, setCount] = useState(()=>{ return 初始值 })
  2. setXXX的参数有两个格式,其中函数式可以用来对state进行叠加

setCount(100)

setCount(initState => { 
  // ...
  return newState
})

demo

// 倒计时demo
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
export default function App () {
  const [count, setCount] = useState(10)
  console.log(count, setCount)
  const hClick = () => {
    console.log(11)
    const time = setInterval(() => {
      setCount((count) => {
        if (count === 1) {
          clearInterval(time)
        }
        return count - 1
      })
    }, 1000)
  }
  return (
    <div>
      <p>倒计时</p>
      <h1>{count}</h1>
      <button onClick={hClick}>倒计时</button>
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))

useState-组件的更新过程

背景

函数组件没有生命周期

更新过程

示例代码

import { useState } from 'react'

const Count = () => {  
  console.log('Count...')
  const [count, setCount] = useState(0)
  return (
    <div>
      {/* 展示状态值 */}
      <h1>useState Hook -> {count}</h1>
      {/* 点击按钮,让状态值 +1 */}
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  )
}

更新过程

函数组件使用 useState hook 后的执行过程,以及状态值的变化:

  • 组件第一次渲染:

    1. 执行该函数中的代码逻辑
    2. 调用 useState(0) 将传入的参数作为状态初始值,即:0
    3. 渲染组件,此时,获取到的状态 count 值为: 0

用户点击按钮,调用 setCount(count + 1) 修改状态,因为状态发生改变,所以,该组件会重新渲染

  • 组件第二次渲染:

    1. 再次执行该组件中的代码逻辑

    2. 再次调用 useState(0)

      1. 此时 React 内部会拿到最新的状态值而非初始值
      2. 该案例中最新的状态值为 1
    3. 再次渲染组件,此时,获取到的状态 count 值为:1

useState 的初始值(参数)只会在组件第一次渲染时生效

const [count, setCount] = useState(()=>{
  console.log('useState...') // 这句只会输出一次
  return 0
})

也就是说,以后的每次渲染,useState 获取到都是最新的状态值。React 组件会记住每次最新的状态值!

小结

状态更新,整个组件的逻辑重新运行一次;

useState只会在组件第一次渲染时使用状态的初值,随后都会使用状态的最新值

useState 这个 Hook 就是用来管理 state 的,它可以让函数组件具有维持状态的能力。也就是说,在一个函数组件的多次渲染之间,这个 state 是共享的。

如何为函数组件提供多个状态?

两种方案

  • 方案1:useState({状态1, 状态2.....})
  • 方案2: useState(状态1) useState(状态2)

推荐使用方案2

调用 useState Hook 多次即可,每调用一次 useState Hook 可以提供一个状态。

注意:

hooks: setXXX(新值) ==> 用新值去替换之前的值

class: setState({要修改的属性})

useState的使用规则

规则

  1. useState只能直接出现在 函数组件 内部

  2. useState不能嵌套在 if/for/

    原因: React 是按照 Hooks 的调用顺序来识别每一个 Hook,如果每次调用的顺序不同,导致 React 无法知道是哪一个 Hook。 可以通过开发者工具进行查看

useEffect-理解副作用

函数式组件:

  • 主作用:就是根据数据(state/props)渲染 UI
  • 副作用:数据(Ajax)请求、手动修改 DOM、开启定时器,清空定时器,添加事件监听,删除事件, localStorage 操作等

总结

对于react组件来说,除了渲染UI之外的其他操作,都可以称之为副作用。

useEffect-基本使用

使用步骤

// 1. 导入useEffect
import { useEffect } from 'react'

// 2. 使用useEffect
useEffect(() => {
	console.log('useEffect 1 执行了,可以做副作用')
})
useEffect(() => {
	console.log('useEffect 2 执行了,可以做副作用')
})

执行时机

render工作完成之后,执行Effect;

如果定义了多个,则顺序执行;

案例

import React, { useState, useEffect } from 'react'
import ReactDom from 'react-dom'
export default function App () {
  useEffect(() => {
    console.log('useEffect')
    document.title = 'count' + count
  })
  const [count, setCount] = useState(0)
  return (
    <div
      onClick={() => {
        setCount(count + 1)
      }}>
      函数组件{count}
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))

useEffect-设置依赖项

useEffect的依赖项

useEffect有两个参数:

参数1: 副作用函数。

参数2:执行副作用函数的依赖项:它决定了什么时机执行参数1(副作用函数)

useEffect的完整格式

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

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

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

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

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

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

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

参考:

  1. useEffect完全指南:overreacted.io/zh-hans/a-c…

useEffect发送请求

格式

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

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

// 错误演示:

// 不要给 useEffect 添加 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
  }
  
}, [])

自定义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
}

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 组件,还缺少了一个很重要的能力:在多次渲染之间共享数据。

问题导入

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 = 你需要在多次渲染之间共享的数据
}

useContext-全局状态

useContext步骤

共3步:

  1. 导入并调用createContext方法,得到Context对象,导出
import { createContext } from 'react'
export const Context = createContext()
  1. 使用 Provider 组件包裹根组件,并通过 value 属性提供要共享的数据
return (
  <Context.Provider value={ 这里放要传递的数据 }>
  	<根组件的内容/>
  </Provider>
)
  1. 在任意后代组件中,如果希望获取公共数据:

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

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

宝,你都看到这了不给我一个star嘛?

PS:  如果内容有错误的地方欢迎指出(觉得看着不理解不舒服想吐槽也完全没问题);如果有帮助,欢迎点赞和收藏,转载请著明出处,如果有问题也欢迎私信交流