React Hooks 学习 - 02 useState、useReducer、useContext、useEffect

useState 钩子函数

介绍

用于为函数组件引入状态。

通常情况下,函数中的变量在函数执行完就会被释放掉,useState 方法通过闭包(将数据存储在组件函数外)让函数型组件实现保存和变更状态。

useState 接收一个初始状态的值作为参数,返回一个数组,数组的第一个元素是状态数据,第二个元素是设置状态数据的方法,该方法接收一个参数用于修改状态数据。

可以通过数组解构的方式将数组中的元素解构出来。

组件重新渲染时,useState 会获取状态的值,忽略设置的初始值。

使用

import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  return <div>
    <span>{count}</span>
    <button onClick={() => setCount(count+1)}>+ 1</button>
  </div>
}

export default App

useState 使用细节

  1. 接收唯一的参数,即状态初始值,初始值可以是任意数据类型
  2. 返回值为数组,数组中存储状态值和更改状态值的方法,方法名称约定以set开头,后面加上状态名称
  3. 方法可以被调用多次,用于保存不同状态值
  4. 参数可以是一个函数,函数返回什么,初始状态就是什么,函数只会被调用一次,用在初始值是动态值得情况
// 参数是函数的场景,优先取组件接收的数据,这样只会在挂载时获取一次 props 中的数据
function App(props) {
  // 错误写法:每次渲染都会获取 props.count
  // const propCount = props.count
  // const [count, setCount] = useState(propCount || 0)
  
  // 正确写法:只会在挂载时执行一次
  const [count, setCount] = useState(() => {
    return props.count || 0
  })
  return <div>
    <span>{count}</span>
    <button onClick={() => setCount(count+1)}>+ 1</button>
  </div>
}

设置状态值方法的使用细节

  • 接收唯一的参数参数可以是一个值也可以是一个回调函数
  • 参数即新的状态值或返回新的状态值的函数,它会完全替换状态值,不会像 setState一样合并对象类型的状态值。
  • 方法本身是异步的,若要同步执行一些业务,业务代码要写在回调函数参数内。
import { useState } from 'react'

function App(props) {
  const [count, setCount] = useState(() => {
    return props.count || 0
  })

  function handlePower(number) {
    setCount(() => {
      const power = number * number

      // 同步代码在 setCount 参数内定义
      // document.title = count

      return power
    })

    // count 为 setCount 执行前的值
    document.title = count
  }
  return <div>
    <span>{count}</span>
    <button onClick={() => setCount(count+1)}>+ 1</button>
    <button onClick={() => handlePower(count)}>求平方</button>
  </div>
}

export default App

useReducer 钩子函数

介绍

useState 的替代方案,是另一种让函数组件引入状态的方式。

useReducer 的方式类似 Redux,组件的状态被保存在特定的地方,要想改变状态,需要通过 dispatch 方法触发一个 Action,这个 Action 会被 Reducer 函数接收,Reducer 内部要根据 Action 的类型决定如何处理状态,最后通过返回值的方式更新状态。

使用

useReducer 方法的参数:

  • 第一个参数:接收一个形如(state, action) => newState 的 Reducer 函数
  • 第二个参数:默认的状态初始值

返回:当前的 state 和配套的 dispatch 方法。

import { useReducer } from 'react'

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return state + 1
    case 'decrement':
      return state - 1
    default:
      return state
  }
}

function App() {
  const [count, dispatch ] = useReducer(reducer, 0)

  return <div>
    <span>{count}</span>
    <button onClick={() => dispatch({type: 'increment'})}>+ 1</button>
    <button onClick={() => dispatch({type: 'decrement'})}>- 1</button>
  </div>
}

export default App

相对于 useState 的好处

  • 对逻辑较复杂且包含多个子值的 state,使用 useReducer 方便根据 Action 的类型修改部分数据。
  • 适合下一个 state 依赖上一个 state 的场景
  • 给那些会触发深更新的组件做性能优化
    • 可以向下级组件传递 dispatch 方法,而不是组件内定义的函数
    • 这样当组件重新渲染时,如果传给下级组件的状态未发生变化,下级组件就不会重新渲染
    • 因为dispatch的引用是固定的,而组件内定义的函数在组件重新渲染时被重新定义,因此也会触发下级组件的渲染

useContext 钩子函数

在使用 createContext 跨组件层级传递数据时,简化获取数据的代码。

useContext 接收一个 context 对象(React.createContext 的返回值),返回该 context 的当前值。

调用了 useContext 的组件总会在 context 值变化时重新渲染。

context 类组件 static 使用

import React, { createContext } from 'react'

const ThemeContext = createContext()

class Foo extends React.Component {
  static contextType = ThemeContext

  render() {
    return <div>{ this.context }</div>
  }
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Foo />
    </ThemeContext.Provider>
  )
}

export default App

Consumer 嵌套组件使用

import { createContext } from 'react'

const ThemeContext = createContext()

function Foo() {
  return <ThemeContext.Consumer>
    {
      theme => <div>{ theme }</div>
    }
  </ThemeContext.Consumer>
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Foo />
    </ThemeContext.Provider>
  )
}

export default App

useContext 使用

使函数型组件中不适用组件嵌套(Consumer)就可以订阅 Context。

import { createContext, useContext } from 'react'

const ThemeContext = createContext()

function Foo() {
  const theme = useContext(ThemeContext)
  return <div>{ theme }</div>
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Foo />
    </ThemeContext.Provider>
  )
}

export default App

useEffect 钩子函数

介绍

让函数型组件拥有处理副作用的能力,类似生命周期函数。

  • 在类组件中使用生命周期函数处理副作用。
  • 在函数型组件中使用 useEffect处理副作用。

useEffect 执行时机

useEffect可以看作 componentDidMountcomponentDidUpdatecomponentWillUnmount 三个函数的组合:

  • useEffect(() => {}) ==> componetDidMount, componentDidUpdate
    • 当组件挂载完成和状态更新完成时会调用传入的函数
  • useEffect(() => {}, []) ==> componetDidMount
    • 第二个参数接收一个数组,用于指定监听的状态,当组件挂载完成和指定的状态变更完成时执行传入的函数
    • 如果第二个参数是空数组,表示没有要监听的状态,则只会在挂载完成时执行一次
  • useEffect(() => () => {}) ==> componetWillUnmount
    • 传入的函数如果返回了一个回调函数,组件更新前和被卸载前会执行这个回调函数
    • 这个函数用于清理上一个副作用,以保证一致性(避免上一个副作用的效果残留)

当组件重新渲染时,useEffect 会用新的副作用函数替换之前的副作用函数。

使用

import { useState, useEffect } from 'react'
import ReactDom from 'react-dom'

function Foo(props) {
  useEffect(() => {
    console.log('1. 组件挂载完成和状态更新完成时执行')
  })

  useEffect(() => {
    console.log('2. 仅在组件挂载完成时执行一次')
  }, [])

  useEffect(() => {
    return () => {
      console.log('3. 在组件更新前和卸载前执行', 'count: '+ count)
    }
  })

  return <div>{props.count}</div>
}

function App(props) {
  const [count, setCount] = useState(0)

  return <div>
    <Foo count={count} />
    <button onClick={() => setCount(count+1)}>+ 1</button>
    <button onClick={() => ReactDom.unmountComponentAtNode(document.getElementById('root'))}>卸载组件</button>
  </div>
}

export default App

ReactDom.unmountComponentAtNode(container) 用于卸载容器中渲染的组件。

示例

  1. 为 window 对象添加滚动事件
  2. 设置定时器,让 count 数值每秒 +1

index.html 文件中给 body 添加高度:

<body style="height: 2000px;">
  <div id="root"></div>
</body>
import { useState, useEffect } from 'react'
import ReactDom from 'react-dom'

function App() {
  function onScroll() {
    console.log('页面滚动了')
  }

  useEffect(() => {
    window.addEventListener('scroll', onScroll)

    return () => {
      window.removeEventListener('scroll', onScroll)
    }
  }, [])

  const [count, setCount] = useState(0)

  useEffect(() => {
    const timerId = setInterval(() => {
      // 这里需使用函数,将 count 作为参数传入,否则数值只会更新一次
      setCount(count => {
        document.title = count + 1
        return count + 1
      })

    }, 1000)

    return () => {
      clearInterval(timerId)
    }
  }, [])

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => ReactDom.unmountComponentAtNode(document.getElementById('root'))}>卸载组件</button>
    </div>
  );
}

export default App;

在处理副作用上 useEffect 相比类组件的优势

  1. 按照用途将代码进行分类
    • 由于 useEffect 可以被多次调用,所以将一组相干的业务逻辑归置到同一个副作用函数中,将不相干的业务逻辑分置到不同的副作用函数中
  2. 简化重复代码,使组件内部代码更加清晰
    • 例如避免在 componentDidMountcomponentDidUpdate 中编写重复代码

useEffect 的第二个参数

useEffect 的第二个参数是依赖项数组,作用是:只有指定数据发生变化时才会触发副作用(effect)。

  • 当不传递依赖项数组的时候,会在组件数据(所有)发生变化的时候触发副作用函数。
  • 如果传递一个空数组,则只会在初始加载时执行副作用函数,不会监听任何数据的变化。

原理是接收一个给定值的数组,组件每次渲染,用新的数组和旧的数组去对比,有任何一项不相等则执行副作用。

import { useState, useEffect } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const [person, setPerson] = useState({name:'张三'})

  useEffect(() => {
    console.log('只有当 count 变化时才会执行回调函数')
    document.title = count
  }, [count])

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+ 1</button>
      <br/>
      <span>{person.name}</span>
      <button onClick={() => setPerson({name: '李四'})}>更名</button>
    </div>
  );
}

export default App;

异步操作

使用 await async 关键字

useEffect 回调函数中执行异步操作,例如:

import { useEffect } from 'react'

function App() {
  useEffect(() => {
    getData().then(result => {
      console.log(result)
    })
  }, [])

  return (
    <div>App</div>
  );
}

// 模拟的异步操作
function getData() {
  return new Promise(resolve => {
    resolve({msg: 'Hello Async'})
  })
}

export default App;

如果想使用await关键字,则需要添加 async关键字:

useEffect(() => {
  const asyncFn = async () => {
    const result = await getData()
    console.log(result)
  }
  asyncFn()
}, [])

但是这样写,会出现问题:

# 在 React 16 中会报错打断运行(Error)
An effect function must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately:<推荐写法>
# 副作用函数必须返回一个用于清理的普通函数。
# 看起来你编写了 useEffect(async () => ...) 或返回了一个 Promise。相反,你可以在副作用函数中编写异步函数,并立即调用它:<推荐写法>
# 在 React 17 中会警告(Warning)
Effect callbacks are synchronous to prevent race conditions. Put the async function inside:<推荐写法>
# 为了防止竞态条件(异步创建的任务无法确定执行顺序),副作用回调函数都是同步的。将异步函数像这样写入:<推荐写法>

错误原因

How to fetch data with React Hooks?

如控制台提示的,副作用函数必须返回一个用做清理资源(组件销毁时调用)的普通函数。

如果使用 async 关键字声明函数,则会声明一个异步函数,异步函数会返回一个 Pormise,副作用函数返回 Promise,违反了使用规则。

React 17 这种写法虽然不会阻塞程序,但也不建议这样使用。

推荐写法

如官方推荐的写法,在普通函数中执行异步操作,将普通函数传给 useEffect

useEffect(() => {
  const asyncFn = async () => {
    const result = await getData()
    console.log(result)
  }
  asyncFn()
}, [])

或在自执行函数中执行:

useEffect(() => {
  (async () => {
    const result = await getData()
    console.log(result)
  })()
}, [])
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值