React Hook之useState、useEffect和useContext

前言

一周的砖又快搬完了,又到了开心快乐的总结时间~这两周一直在 hook 函数的“坑”里,久久不能自拔。应该也不能叫作“坑”吧,仍是本身太菜了,看文档不仔细,不少觉得不重要,但在实际应用中却很关键的点老是被本身忽略。因此我准备多花点时间,把官网的一些 hook 函数,再回过头看一遍,整理整理(在整理的过程,我以为更容易发现问题和总结经验)。html

这篇文章主要整理一下 React 中的三个基础 Hook:前端

  • useState
  • useEffect
  • useContext

useState

useState 相比其余 hooks 仍是很简单的,主要就是用来定义变量。官方文档描述的也很清楚,对此已经很熟练的看官大大能够跳过哦~react

相遇--初识

const [count, setCount] = useState(0)
复制代码
  • 调用 useState 方法作了什么?git

    定义一个“state变量”。github

  • useState 须要什么参数?npm

    useState 方法接收一个参数,做为变量初始化的值。(示例中调用 useState 方法声明一个 “state变量” count,默认值为 0。)redux

  • useState 方法的返回值是什么?数组

    返回当前 state 以及更新 state 的函数。浏览器

相知--使用useState

React 会确保 setState 函数的标识是稳定的,而且不会在组件从新渲染时发生变化。这就是为何能够安全地从 useEffect 或 useCallback 的依赖列表中省略 setState。安全

首先咱们先经过 useState 方法定义三个变量(包含基本类型和引用类型的数据)分别为:countstudentInfosubjectList,而后对它们的值进行修改。

const [count, setCount] = useState(0)

const [studentInfo, setStudentInfo] = useState({name: '小文', age: 18, gender: '女'})

const [subjectList, setSubjectList] = useState([
  { id: 0, project_name: '语文' },
  { id: 1, project_name: '数学' }
])
复制代码
  • 修改 count 值为1
setCount(1)
复制代码
  • 修改 studentInfo 对象的 age 属性,值为 20;并添加 weight 属性,值为 90
setStudentInfo({
  ...studentInfo,
  age: 20,
  weight: 90
})
复制代码
  • 修改 subjectList 数组的第二项的 project_name 属性,值为体育;并添加第三项 { id: 2, project_name: '音乐' }
# 忽略这里的深拷贝,优雅的方式有不少:immutable.js、immer.js、loadsh
let temp_subjectList = JSON.parse(JSON.stringify(subjectList))
temp_subjectList[1].project_name = '体育'
temp_subjectList[2] = { id: 2, project_name: '音乐' }
setSubjectList(temp_subjectList)
复制代码

咱们在实际的开发中,会用到 React 提供的 Eslint 插件来检查 Hook 的规则和 effect 的依赖,当检测出某一块的代码缺乏依赖时,会给出警告,若是给出的警告是缺乏 setState 函数,那咱们就能够忽略它。(后面讲到 useEffect 的时候会再补充)

React 会确保 setState 函数的标识是稳定的,而且不会在组件从新渲染时发生变化。这就是为何能够安全地从 useEffectuseCallback 的依赖列表中省略 setState。(官网)

函数式更新

若是新的 state 须要经过使用先前的 state 计算得出,那么能够将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。(对于引用类型的数据,上面同样,例如数组。也是不能够直接对变量进行操做的)

使用上面定义的变量:

  • 点击按钮累加 count
<button onClick={() => setCount(prevCount => ++prevCount)}>+ 累加</button>
复制代码
  • 修改 studentInfo 对象的 age 属性,值为 20
setStudentInfo(prevState => {
  # 也可使用 Object.assign
  return {...prevState, age: 20}
})
复制代码

惰性初始 state

若是初始 state 须要经过复杂计算得到,则能够传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props)
  return initialState
})
复制代码

实际应用

咱们在实际应用中,常常会遇到一些结构比较复杂的数据,若是每一个地方都使用 useState 去定义这些复杂结构的数据,估计会累死。

这里就分享一下我在项目中使用的一个插件 use-immer,一些基本类型数据和“直接替换的数据”(例若有一个数组 arr,对 arr 修改是直接赋值 setArr([...]),这样的数据我会选择用 useState 来声明,对于大部分引用类型的数据我会使用 use-immer 提供的 useImmer 方法来声明。下面咱们就来看看它是如何使用的吧~

  1. 安装
npm install immer use-immer
复制代码
  1. 引用
import { useImmer } from 'use-immer'
复制代码
  1. 从新声明上面用到的 subjectList
const [subjectList, setSubjectList] = useImmer([
  { id: 0, project_name: '语文' },
  { id: 1, project_name: '数学' }
])
复制代码
  1. 修改 subjectList 数组的第二项的 project_name 属性,值为体育;并添加第三项 { id: 2, project_name: '音乐' }
setSubjectList(draft => {
  draft[1].project_name = '体育'
  draft[2] = { id: 2, project_name: '音乐' }
})
复制代码

须要注意的是,这里的 setSubjectList 方法接收的是一个函数,该函数接收一个参数 draft,能够理解为是变量 subjectList 的副本。这种写法是否是有种 “家” 的感受呢,感兴趣的能够深刻了解一下哦(immutable、immer、use-immer)。

useEffect

关于 useEffect 函数,我我的的建议是先把官网上的介绍看一遍,再多研读研读《useEffect 完整指南》。看完会发现,咱们对它已经有了更加深入的认识。这里也仅仅是我在学习的过程整理的笔记,内容就是《useEffect 完整指南》简化。

useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。

每一次渲染

重点:关于每一次渲染(rendering),组件都会拥有本身的:

  1. Props and State
  2. 事件处理函数
  3. Effects

Props and State

写一个计数器组件 Counter

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}
复制代码

Counter组件第一次渲染的时候,从 useState() 拿到 count 的初始值为 0。当咱们调用 setCount(1)时,React 会再次渲染该组件,此时 count 的值为 1,以此类推,每一次渲染都是独立的。

# During first render
function Counter() {
  const count = 0; # Returned by useState()
  # ...
  <p>You clicked {count} times</p>
  # ...
}

# After a click, our function is called again
function Counter() {
  const count = 1; # Returned by useState()
  # ...
  <p>You clicked {count} times</p>
  # ...
}

# After another click, our function is called again
function Counter() {
  const count = 2; # Returned by useState()
  # ...
  <p>You clicked {count} times</p>
  # ...
}
复制代码

Counter 组件中的 count 仅仅是一个常量,这个常量由 React 提供。当调用 setCount 的时候,React 会带着一个不一样的 count 值再次调用组件。而后,React会更新DOM以保持和渲染输出一致。

最关键 的就是:任意一次渲染中的 count 常量都不会随着时间改变。渲染输出会变是由于 Counter 组件被调用,而在每一次调用引发的渲染中,它包含的 count 常量都是独立的。也就是说组件的每次渲染,props 和 state 都是独立的。

事件处理函数

修改一下计数器组件 Counter 的例子。

组件内容:有两个按钮,一个按钮用来修改 count 的值,另外一个按钮在 3s 延迟后展现弹窗。

function Counter() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count)
    }, 3000)
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  )
}
复制代码
  • 点击按钮修改 count 的值为 3。
  • 点击另外一个按钮打开弹窗。
  • 在弹窗弹出前,点击按钮修改 count 的值为 5。

此时,弹窗中的展现的 count 值为 3。

分析:

首先整个过程进行了 6 次渲染。

  1. 初始化渲染:render0;
  2. 修改 count 值为 3,进行 3 次渲染:render1 -> render2 -> render3;
  3. 点击按钮打开弹窗,此时组件是 “render3 状态”;
  4. 修改 count 值为 5,进行 2 次渲染:render3 -> render5 -> render5;
# 组件状态:render0 -> render1 -> render2 -> render3
function Counter() {
  const count = 3
  # ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count)
    }, 3000)
  }
  # ...
}

# 组件状态: render3
# 触发事件处理函数 handleAlertClick,此时该函数捕获 count 值为 3,并将在 3 秒后打开弹窗。
function Counter() {
  const count = 3
  # ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 3)
    }, 3000)
  }
  # ...
}

# 组件状态:render3 -> render5 -> render5
function Counter() {
  const count = 5
  # ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count)
    }, 3000)
  }
  # ...
}
复制代码

Effects

修改 Counter 组件,点击 3 次按钮:

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

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`)
    }, 3000)
  })

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}
复制代码

分析: 整个过程组件进行了四次渲染:

  1. 初始化,render0:打印 You clicked 0 times
  2. 修改 count 值为1,render1:打印 You clicked 1 times
  3. 修改 count 值为2,render2:打印 You clicked 2 times
  4. 修改 count 值为3,render3:打印 You clicked 3 times

经过整个例子咱们能够知道,在每次渲染中,useEffect 也是独立的。

并非 coun t的值在“不变”的 effect 中发生了改变,而是 effect 函数自己在每一次渲染中都不相同。

清除 effect

当咱们在 useEffect 中使用了定时器或者添加了某些订阅,能够经过 useEffect 返回一个函数,进行清除定时器或者取消订阅等操做。但咱们须要知道的是,清除是 “滞后” 的。(这里是我的的理解,可能描述的不许确)

看一下例子,在 useEffect 中打印点击的次数:

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

  useEffect(() => {
    console.log(`You clicked ${count} times`)
    return() => {
      console.log('销毁')
    }
  })

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}
复制代码

点击按钮 3 次,控制台中打印的结果以下:

  1. You clicked 0 times
  2. 销毁
  3. You clicked 1 times
  4. 销毁
  5. You clicked 2 times
  6. 销毁
  7. You clicked 3 times

从打印结果咱们能够很容易看出,上一次的 effect 是在从新渲染时被清除的。

补充:那么组件的整个从新渲染的过程是怎么样的呢?

假设如今有 render0 和 render1 两次渲染:

  1. React 渲染 render1 的UI;
  2. 浏览器绘制,并呈现 render1 的UI;
  3. React 清除 render0 的 effect;
  4. React 运行 render1 的 effect;

React 只会在浏览器绘制后运行 effects。这使得你的应用更流畅由于大多数effects并不会阻塞屏幕的更新。

经过下面这个例子,来印证一下这个结论吧~

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

  useEffect(() => {
    setCount(99)
    console.log(count)
    return() => {
      console.log('销毁')
    }
  })

  console.log('我确定最早执行!')

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}
复制代码

运行代码,在控制台咱们能够看到如下输出(此时组件状态为初始化:render0):

  1. 我确定最早执行!
  2. 0
  3. 我确定最早执行!
  4. 销毁
  5. 99
  6. 我确定最早执行!

根据打印结果,咱们能够分析出:

  1. React 渲染初始化的UI(render0);
  2. 执行 useEffect
  3. 调用 setCount 方法修改 count 值为 99,组件从新渲染(render1);
  4. 根据执行顺序,继续执行 render0 状态下的 useEffect,打印 count 的值为 0。(render0 下 count 值为0)
  5. React 从新渲染UI(render1);
  6. 执行 render0 的 useEffect 的清除函数;
  7. 执行 render1 的 useEffect
  8. 调用 setCount 方法修改 count 值为 99(因为传入的值没有改变,因此组件没有从新渲染);
  9. 打印 count 的值为 99;

其中 我确定最早执行!,这个打印我理解的是:组件被调用了,React 判断是否须要渲染。而后才有了上面的一系列步骤,若是理解有误还请帮忙指出。

设置依赖

实际应用中,咱们不须要在每次组件更新时,都去执行某些 effects,这个时候咱们能够给 useEffect 设置依赖,告诉 React 何时去执行 useEffect

看下面这个例子,只有在 name 发生改变时,才会执行这个 useEffect。若是将依赖设置为空数组,那么这个 useEffect 只会执行一次。

useEffect(() => {
  document.title = 'Hello, ' + name
}, [name])
复制代码

正确地设置依赖

引出问题:首先需求很简单,经过定时器,每过一秒就将 count 的值累加 1。

const [count, setCount] = useState(0)

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1)
}, 1000)
  return () => clearInterval(id)
}, [])
复制代码

咱们只但愿设置一次 setInterval 定时器,因此将依赖设置为了 [],可是因为组件每次渲染拥有独立的 state 和 effects,因此上面代码中的 count 值,一直是 0,当一次执行完 setCount后,后续的setCount操做都是无效的。

那既然这样,咱们能够在依赖里面添加依赖 count就能够解决问题了吧?思路是正确的,可是这样就违背了咱们 “咱们只但愿 setInterval 执行一次” 的初衷,且极可能形成一些没必要要的bug。

解决方案:使用函数式更新(前面有讲到的哦)。

const [count, setCount] = useState(0)

useEffect(() => {
  const id = setInterval(() => {
    setCount(preCount +> preCount + 1)
}, 1000)
  return () => clearInterval(id)
}, [])
复制代码

可是在实际应用中,这种方式还远远不能知足咱们的需求。好比在依赖多个数据的时候:

function Counter() {
  const [count, setCount] = useState(0)
  const [step, setStep] = useState(1)

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + step)
    }, 1000);
    return () => clearInterval(id)
  }, [step])

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  )
}
复制代码

当咱们在修改 step 变量时,会从新设置定时器。这是咱们不肯意看到的,那应该怎么去优化呢?这个时候咱们就须要用到useReducer了。

当咱们想更新一个状态,而且这个状态更新依赖于另外一个状态的值时,咱们可能须要使用useReducer去替换它们。

import React, { useReducer, useEffect } from 'react'
import ReactDOM from 'react-dom'

const initialState = {
  count: 0,
  step: 1,
}

function reducer(state, action) {
  const { count, step } = state
  if (action.type === 'tick') {
    return { count: count + step, step }
  } else if (action.type === 'step') {
    return { count, step: action.step }
  } else {
    throw new Error()
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)
  const { count, step } = state

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' })
    }, 1000)
    return () => clearInterval(id)
  }, [dispatch])

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        })
      }} />
    </>
  )
}
复制代码

React 会保证 dispatch 在组件的声明周期内保持不变。

关于函数

useEffect 中调用了定义在外部的函数时,咱们可能会遗漏依赖。因此咱们能够将函数的定义放到useEffect中。

可是当咱们有一些可复用的函数定义在外部,此时应该怎么处理呢?

  1. 若是这个函数没有使用组件内的任何值,咱们能够将它放到组件外部定义。
function getFetchUrl(query) {
  return 'xxx?query=' + query
}

function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl('react')
  }, [])

  useEffect(() => {
    const url = getFetchUrl('redux')
  }, [])
}
复制代码
  1. 使用 useCallback 包装。
function SearchResults() {
  const [query, setQuery] = useState('react')

  const getFetchUrl = useCallback(() => {
    return 'xxx?query=' + query
  }, [query])

  useEffect(() => {
    const url = getFetchUrl()
  }, [getFetchUrl])
}
复制代码

若是 query 保持不变,getFetchUrl也会保持不变,咱们的 effect 也不会从新运行。可是若是 query 修改了,getFetchUrl 也会随之改变,所以会从新请求数据。

useCallback本质上是添加了一层依赖检查。它以另外一种方式解决了问题 - 咱们使函数自己只在须要的时候才改变,而不是去掉对函数的依赖。

关于 useReduceruseCallback 的更多内容,我会在后面的笔记中整理出来。

useContext

useContext的使用场景。

在一个典型的 React 应用中,数据是经过 props 属性自上而下(由父及子)进行传递的,但这种作法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都须要的。Context 提供了一种在组件之间共享此类值的方式,而没必要显式地经过组件树的逐层传递 props。(引用自官网)

直接看一个例子吧~

  1. 建立顶层组件 Container
import React, { useState, createContext } from 'react'
import Child1 from './Child1'
import Child2 from './Child2'

// 建立一个 Context 对象
export const ContainerContext = createContext({})

function Container() {
  const [state, setState] = useState({child1Color: 'pink', child2Color: 'skyblue'})
  const changeChild1Color = () => {
    setState({
      ...state,
      child1Color: 'lightgreen'
    })
  }

  return (
    <>
      <ContainerContext.Provider value={state}>
        <Child1></Child1>
        <Child2></Child2>
      </ContainerContext.Provider>
      <button onClick={changeChild1Color}>修改child1颜色</button>
    </>
  )
}

export default Container
复制代码
  1. 建立子组件Child1
import React, {useContext} from 'react'
import { ContainerContext } from './Container'

function Child1() {
  const value = useContext(ContainerContext)
  return <h1 style={{color: value.child1Color}}>我是Child1组件</h1>
}

export default Child1
复制代码
  1. 建立子组件Child2
import React, {useContext} from 'react'
import { ContainerContext } from './Container'

function Child2() {
  const value = useContext(ContainerContext)
  return <h1 style={{color: value.child2Color}}>我是Child2组件</h1>
}

export default Child2
复制代码

咱们能够经过这个简单的 demo 来了解一下 useContext 相关的基础知识。

基本使用方法分析:

  1. 经过 React 提供的 createContext 方法建立一个 Context 对象。

    Container组件中,经过export const ContainerContext = createContext({})建立了一个 Context 对象,并设置了默认值为 {}

    注意:默认值只有在组件所处的树中没有匹配到 Provider 时,默认值才会生效。

    稍微修改一下 Container 中返回的组件,这个时候 Child1Child2 读取的 context 就是建立 Context 对象时的默认值了。

    <>
      <Child1></Child1>
      <Child2></Child2>
      <button onClick={changeChild1Color}>修改child1颜色</button>
    </>
    复制代码
  2. 每一个 Context 对象都会返回一个 Provider React 组件,它容许消费组件订阅 context 的变化。

    Container 组件中的 Context 对象返回 ContainerContext.Provider 组件,它接收一个value 属性,传递给消费组件。同时包裹在其内部的消费组件(Child1Child2)能够订阅 context 的变化。

    一个 Provider React 组件能够和多个消费组件有对应关系。多个 Provider React 组件 也能够嵌套使用,里层的会覆盖外层的数据。

  3. 在消费组件中,使用 useContext 订阅 context。

    注意useContext 的参数必须是 context 对象自己。

基本使用方式就是这样了,因为useContext我用的还比较少,这里就先不作过多的介绍了。

值得注意的是:

  1. 订阅了 context 的组件,总会在 context 值变化时从新渲染。若是重渲染组件的开销较大,能够经过使用 memoization 来优化。
  2. 使用 Context 必定程度上会使组件的复用性下降,咱们须要合理的取舍。

总结

学习的过程当中,我常常会有种错觉:我会了。其实这种“会”,也只是对某个知识点的“眼熟”。真正须要动手去完成的时候,就会发现一头雾水。就像刚开始接触前端的时候,看到别人的代码,总会恍然大悟,但本身却写不出来同样。再加上不少知识点学过的那两天能够记得,可是一段时间不用就会遗忘,又要从新学习。

因此我想要改变这种困境,经过整理本身的学习过程,加深印象的同时也方便之后查阅。

但愿这篇文章对你一样也有所帮助,若是有建议欢迎留言~

啰嗦了这么多,小伙伴们给我留个赞吧,谢谢~

参考文章:

useEffect 完整指南
react 官网

相关文章
相关标签/搜索