超详细React Hook实践指南

React为何须要Hook这篇文章中咱们探讨了React开发团队为何要为Function Component添加Hook的缘由,在本篇文章中我将会为你们提供一份较为全面的React Hook实践指南,其中包括如下方面的内容:javascript

  • 什么是React Hook
  • 经常使用Hook介绍
    • useState
    • useEffect
    • useRef
    • useCallback
    • useMemo
    • useContext
    • useReducer
  • 自定义Hook

什么是React Hook

React Hook是React 16.8版本以后添加的新属性,用最简单的话来讲,React Hook就是一些React提供的内置函数,这些函数可让Function Component和Class Component同样可以拥有组件状态(state)以及进行反作用(side effect)html

经常使用Hook介绍

接下来我将会为你们介绍一些经常使用的Hook,对于每个Hook,我都会覆盖如下方面的内容:java

  • 做用
  • 用法
  • 注意事项

useState

做用

useState理解起来很是简单,和Class Component的this.state同样,都是用来管理组件状态的。在React Hook没出来以前,Function Component也叫作Functional Stateless Component(FSC),这是由于Function Component每次执行的时候都会生成新的函数做用域因此同一个组件的不一样渲染(render)之间是不可以共用状态的,所以开发者一旦须要在组件中引入状态就须要将原来的Function Component改为Class Component,这使得开发者的体验十分很差。useState就是用来解决这个问题的,它容许Function Component将本身的状态持久化到React运行时(runtime)的某个地方(memory cell),这样在组件每次从新渲染的时候均可以从这个地方拿到该状态,并且当该状态被更新的时候,组件也会重渲染react

用法

const [state, setState] = useState(initialState)
复制代码

useState接收一个initialState变量做为状态的初始值,返回值是一个数组。返回数组的第一个元素表明当前state的最新值,第二个元素是一个用来更新state的函数。这里要注意的是statesetState这两个变量的命名不是固定的,应该根据你业务的实际状况选择不一样的名字,能够是textsetText,也能够是widthsetWidth这类的命名。(对上面数组解构赋值不熟悉的同窗能够看下MDN的介绍)。ios

咱们在实际开发中,一个组件可能不止一个state,若是组件有多个state,则能够在组件内部屡次调用useState,如下是一个简单的例子:git

import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const App = () => {
  const [counter, setCounter] = useState(0)
  const [text, setText] = useState('')

  const handleTextChange = (event) => {
    setText(event.target.value)
  }

  return (
    <>
      <div>Current counter: {counter}</div>
      <button
        onClick={() => setCounter(counter + 1)}
      >
        Increase counter
      </button>
      <input
        onChange={handleTextChange}
        value={text}
      />
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
复制代码

和Class Component的this.setState API相似,setCountersetText均可以接收一个函数为参数,这个函数叫作updaterupdater接收的参数是当前状态的最新值,返回值是下一个状态。例如setCounter的参数能够改为一个函数:github

<button
  onClick={() => {
    setCounter(counter => counter + 1)
  }}
>
  Increase counter
</button>
复制代码

useStateinitialState也能够是一个用来生成状态初始值的函数,这种作法主要是避免组件每次渲染的时候initialState须要被重复计算。下面是个简单的例子:express

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

注意事项

setState是全量替代

Function Component的setState和Class Component的this.setState函数的一个重要区别是this.setState函数是将当前设置的state浅归并(shallowly merge)到旧state的操做。而setState函数则是将新state直接替换旧的state(replace)。所以咱们在编写Function Component的时候,就要合理划分state,避免将没有关系的状态放在一块儿管理,例以下面这个是很差的设计:npm

const [state, setState] = useState({ left: 0, top: 0, width: 0, height: 0 })
复制代码

在上面代码中,因为咱们将互不关联的DOM位置信息{left: 0, top: 0}和大小信息{width: 0, height: 0}绑定在同一个state,因此咱们在更新任意一个状态的时候也要维护一下另一个状态:json

const handleContainerResize = ({ width, height }) => {
  setState({...state, width, height})
}

const handleContainerMove = ({ left, top }) => {
  setState({...state, left, top})
}
复制代码

这种写法十分不方便并且容易引起bug,更加合理的作法应该是将位置信息和大小信息放在两个不一样的state里面,这样能够避免更新某个状态的时候要手动维护另外一个状态:

// separate state into position and size states
const [position, setPosition] = useState({ left: 0, top: 0 })
const [size, setSize] = useState({ width: 0, height: 0})

const handleContainerResize = ({ width, height }) => {
  setSize({width, height})
}

const handleContainerMove = ({ left, top }) => {
  setPosition({left, top})
}
复制代码

若是你确实要将多个互不关联的状态放在一块儿的话,建议你使用useReducer来管理你的状态,这样你的代码会更好维护。

设置相同的state值时setState会bailing out of update

若是setState接收到的新的state当前的state是同样的(判断方法是Object.is),React将不会从新渲染子组件或者触发side effect。这里要注意的是虽然React不会渲染子组件,不过它仍是会从新渲染当前的组件的,若是你的组件渲染有些很耗性能的计算的话,能够考虑使用useMemo来优化性能。

setState没有回调函数

不管是useState仍是Class Component的this.setState都是异步调用的,也就是说每次组件调用完它们以后都不能拿到最新的state值。为了解决这个问题,Class Component的this.setState容许你经过一个回调函数来获取到最新的state值,用法以下:

this.setState(newState, state => {
  console.log("I get new state", state)
})
复制代码

而Function Component的setState函数不存在这么一个能够拿到最新state的回调函数,不过咱们可使用useEffect来实现相同的效果,具体能够参见StackOverflow的这个讨论

useEffect

做用

useEffect是用来使Function Component也能够进行反作用的。那么什么是反作用呢?咱们能够先来看看维基百科的定义:

In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation.

通俗来讲,函数的反作用就是函数除了返回值外对外界环境形成的其它影响。举个例子,假如咱们每次执行一个函数,该函数都会操做全局的一个变量,那么对全局变量的操做就是这个函数的反作用。而在React的世界里,咱们的反作用大致能够分为两类,一类是调用浏览器的API,例如使用addEventListener来添加事件监听函数等,另一类是发起获取服务器数据的请求,例如当用户卡片挂载的时候去异步获取用户的信息等。在Hook出来以前,若是咱们须要在组件中进行反作用的话就须要将组件写成Class Component,而后在组件的生命周期函数里面写反作用,这其实会引发不少代码设计上的问题,具体你们能够查看个人上篇文章React为何须要Hook。Hook出来以后,开发者就能够在Function Component中使用useEffect来定义反作用了。虽然useEffect基本能够覆盖componentDidMountcomponentDidUpdatecomponentWillUnmount等生命周期函数组合起来使用的全部场景,可是useEffect和生命周期函数的设计理念仍是存在本质上的区别的,若是一味用生命周期函数的思考方式去理解和使用useEffect的话,可能会引起一些奇怪的问题,你们有兴趣的话,能够看看React核心开发Dan写的这篇文章:A Complete Guide to useEffect,里面阐述了使用useEffect的一个比较正确的思考方式(mental model)。

用法

useEffect(effect, dependencies?)
复制代码

useEffect的第一个参数effect是要执行的反作用函数,它能够是任意的用户自定义函数,用户能够在这个函数里面操做一些浏览器的API或者和外部环境进行交互,这个函数会在每次组件渲染完成以后被调用,例以下面是一个简单的例子:

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

const UserDetail = ({ userId }) => {
  const [userDetail, setUserDetail] = useState({})

  useEffect(() => {
    fetch(`https://myapi/users/${userId}`)
      .then(response => response.json())
      .then(user => setUserDetail(userDetail))
  })

  return (
    <div> <div>User Name: {userDetail.name}</div> </div>
  )
}

ReactDOM.render(<UserDetail />, document.getElementById('root')) 复制代码

上面定义的获取用户详情信息的反作用会在UserDetail组件每次完成渲染后执行,因此当该组件第一次挂载的时候就会向服务器发起获取用户详情信息的请求而后更新userDetail的值,这里的第一次挂载咱们能够类比成Class Component的componentDidMount。但是若是试着运行一下上面的代码的话,你会发现代码进入了死循环:组件会不断向服务端发起请求。出现这个死循环的缘由是useEffect里面调用了setUserDetail,这个函数会更新userDetail的值,从而使组件重渲染,而重渲染后useEffecteffect继续被执行,进而组件再次重渲染。。。为了不重复的反作用执行,useEffect容许咱们经过第二个参数dependencies来限制该反作用何时被执行:指明了dependencies的反作用,只有在dependencies数组里面的元素的值发生变化时才会被执行,所以若是要避免上面的代码进入死循环咱们就要将userId指定为咱们定义的反作用的dependencies

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

const UserDetail = ({ userId }) => {
  const [userDetail, setUserDetail] = useState({})

  useEffect(() => {
    fetch(`https://myapi/users/${userId}`)
      .then(response => response.json())
      .then(user => setUserDetail(userDetail))
  }, [userId])

  return (
    <div> <div>User Name: ${userDetail.name}</div> </div>
  )
}

ReactDOM.render(<UserDetail />, document.getElementById('root')) 复制代码

除了发起服务端的请求外,咱们每每还须要在useEffect里面调用浏览器的API,例如使用addEventListener来添加浏览器事件的监听函数等。咱们一旦使用了addEventListener就必须在合适的时候调用removeEventListener来移除对事件的监听,不然会有性能问题,useEffect容许咱们在反作用函数里面返回一个cleanup函数,这个函数会在组件从新渲染以前被执行,咱们能够在这个返回的函数里面移除对事件的监听,下面是一个具体的例子:

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

const WindowScrollListener = () => {
  useEffect(() => {
    const handleWindowScroll = () => console.log('yean, window is scrolling!')
    window.addEventListener('scroll', handleWindowScroll)

    // this is clean up function
    return () => {
      window.removeEventListener(handleWindowScroll)
    }
  }, [])

  return (
    <div> I can listen to the window scroll event! </div>
  )
}

ReactDOM.render(<WindowScrollListener />, document.getElementById('root')) 复制代码

上面的代码中咱们会在WindowScrollListener组件首次渲染完成后注册一个监听页面滚动事件的函数,并在组件下一次渲染前移除该监听函数。因为咱们指定了一个空数组做为这个反作用的dependencies,因此这个反作用只会在组件首次渲染时被执行一次,而它的cleanup函数只会在组件unmount时才被执行,这就避免了频繁注册页面监听函数从而影响页面的性能。

注意事项

避免使用“旧的”变量

咱们在实际使用useEffect的过程当中可能遇到最多的问题就是咱们的effect函数被调用的时候,拿到的某些state, props或者是变量不是最新的变量而是以前旧的变量。出现这个问题的缘由是:咱们定义的反作用其实就是一个函数,而JS的做用域是词法做用域,因此函数使用到的变量值是它被定义时就肯定的,用最简单的话来讲就是,useEffect的effect会记住它被定义时的外部变量的值,因此它被调用时使用到的值可能不是最新的值。解决这个问题的办法有两种,一种是将那些你但愿每次effect被调用时拿到的都是最新值的变量保存在一个ref里面,而且在每次组件渲染的时候更新该ref的值:

const [someState, setSomeState] = useState()
const someStateRef = useRef()

someStateRef.current = someState

useEffect(() => {
  ...
  const latestSomeState = someStateRef.current
  console.log(latestSomeState)
}, [otherDependencies...])
复制代码

这种作法虽然不是很优雅,不过能够解决咱们的问题,若是你没有了解过useRef用法的话,能够查看本篇文章useRef这部份内容。解决这个问题的另一个作法是将反作用使用到的全部变量都加到effect的dependencies中去,这也是比较推荐的作法。在实际开发中咱们可使用facebook自家的eslint-plugin-react-hooksexhaustive-deps规则来进行编码约束,在你的项目加上这个约束以后,在代码开发阶段eslint就会告诉你要将someState放到useEffectdependencies中去,这样就能够不使用useRef来存储someState的值了,例以下面代码:

const [someState, setSomeState] = useState()

useEffect(() => {
  ...
  console.log(someState)
}, [otherDependencies..., someState])
复制代码

useRef

做用

useRef是用来在组件不一样渲染之间共用一些数据的,它的做用和咱们在Class Component里面为this赋值是同样的。

用法

const refObject = useRef(initialValue)
复制代码

useRef接收initialValue做为初始值,它的返回值是一个ref对象,这个对象的.current属性就是该数据的最新值。使用useRef的一个最简单的状况就是在Function Component里面存储对DOM对象的引用,例以下面这个例子:

import { useRef, useEffect } from 'react'
import ReactDOM from 'react-dom'

const AutoFocusInput = () => {
  const inputRef = useRef(null)

  useEffect(() => {
    // auto focus when component mount
    inputRef.current.focus()
  }, [])

  return (
    <input ref={inputRef} type='text' /> ) } ReactDOM.render(<AutoFocusInput />, document.getElementById('root')) 复制代码

在上面代码中inputRef其实就是一个{current: inputDomInstance}对象,只不过它能够保证在组件每次渲染的时候拿到的都是同一个对象。

注意事项

更新ref对象不会触发组件重渲染

useRef返回的ref object被从新赋值的时候不会引发组件的重渲染,若是你有这个需求的话请使用useState来存储数据。

useCallback

做用

随着Hook的出现,开发者开始愈来愈多地使用Function Component来开发需求。当开发者在定义Function Component的时候每每须要在函数体内定义一些内嵌函数(inline function),这些内嵌函数会在组件每次从新渲染的时候被从新定义,若是它们做为props传递给了子组件的话,即便其它props的值没有发生变化,它都会使子组件从新渲染,而无用的组件重渲染可能会产生一些性能问题。每次从新生成新的内嵌函数还有另一个问题就是当咱们把内嵌函数做为dependency传进useEffectdependencies数组的话,由于该函数频繁被从新生成,因此useEffect里面的effect就会频繁被调用。为了解决上述问题,React容许咱们使用useCallback记住(memoize)当前定义的函数,并在下次组件渲染的时候返回以前定义的函数而不是使用新定义的函数。

用法

const memoizedCallback = useCallback(callback, dependencies)
复制代码

useCallback接收两个参数,第一个参数是须要被记住的函数,第二个参数是这个函数的dependencies,只有dependencies数组里面的元素的值发生变化时useCallback才会返回新定义的函数,不然useCallback都会返回以前定义的函数。下面是一个简单的使用useCallback来优化子组件频繁被渲染的例子:

import React, { useCallback } from 'react'
import useSearch from 'hooks/useSearch'
import ReactDOM from 'react-dom'

// this list may contain thousands of items, so each re-render is expensive
const HugeList = ({ items, onClick }) => {
  return (
    <div> { items.map((item, index) => ( <div key={index} onClick={() => onClick(index)} > {item} </div> )) } </div>
  )
}

const MemoizedHugeList = React.memo(HugeList)

const SearchApp = ({ searchText }) => {
  const handleClick = useCallback(item => {
    console.log('You clicked', item)
  }, [])
  const items = useSearch(searchText)

  return (
    <MemoizedHugeList items={items} onClick={handleClick} /> ) } ReactDOM.render(<SearchApp />, document.getElementById('root')) 复制代码

上面的例子中我定义了一个HugeList组件,因为这个组件须要渲染一个大的列表(items),因此每次重渲染都是十分消耗性能的,所以我使用了React.memo函数来让该组件只有在onClick函数和items数组发生变化的时候才被渲染,若是你们对React.memo不是很熟悉的话,能够看看我写的这篇文章。接着我在SearchApp里面使用MemoizedHugeList,因为要避免该组件的重复渲染,因此我使用了useCallback来记住定义的handleClick函数,这样在组件后面渲染的时候,handleClick变量指向的都是同一个函数,因此MemorizedHugeList只有在items发生变化时才会从新渲染。这里要注意的是因为个人handleClick函数没有使用到任何的外部依赖因此它的dependencies才是个空数组,若是你的函数有使用到外面的依赖的话,记得必定要将该依赖放进useCallbackdependencies参数中,否则会有bug发生。

注意事项

避免在函数里面使用“旧的”变量

useEffect相似,咱们也须要将全部在useCallback的callback中使用到的外部变量写到dependencies数组里面,否则咱们可能会在callback调用的时候使用到“旧的”外部变量的值。

不是全部函数都要使用useCallback

Performance optimizations are not free. They ALWAYS come with a cost but do NOT always come with a benefit to offset that cost.

任何优化都会有代价useCallback也是同样的。当咱们在Function Component里面调用useCallback函数的时候,React背后要作一系列计算才能保证当dependencies不发生变化的时候,咱们拿到的是同一个函数,所以若是咱们滥用useCallback的话,并不会带来想象中的性能优化,反而会影响到咱们的性能,例以下面这个例子就是一个很差的使用useCallback的例子:

import React, { useCallback } from 'react'
import ReactDOM from 'react-dom'

const DummyButton = () => {
  const handleClick = useCallback(() => {
    console.log('button is clicked')
  }, [])

  return (
    <button onClick={handleClick}> I'm super dummy </button>
  )
}

ReactDOM.render(<DummyButton />, document.getElementById('root')) 复制代码

上面例子使用的useCallback没有起到任何优化代码性能的做用,由于上面的代码执行起来其实至关于下面的代码:

import React, { useCallback } from 'react'
import ReactDOM from 'react-dom'

const DummyButton = () => {
  const inlineClick = () => {
    console.log('button is clicked')
  }
  const handleClick = useCallback(inlineClick, [])

  return (
    <button onClick={handleClick}> I'm super dummy </button>
  )
}

ReactDOM.render(<DummyButton />, document.getElementById('root')) 复制代码

从上面的代码咱们能够看出,即便咱们使用了useCallback函数,浏览器在执行DummyButton这个函数的时候仍是须要建立一个新的内嵌函数inlineClick,这和不使用useCallback的效果是同样的,并且除此以外,优化后的代码因为还调用了useCallback函数,因此它消耗的计算资源其实比没有优化以前还多,并且因为useCallback函数内部存储了一些额外的变量(例如以前的dependencies)因此它消耗的内存资源也会更多。所以咱们并不能一味地将全部的内嵌函数使用useCallback来包裹,只对那些真正须要被记住的函数使用useCallback

useMemo

做用

useMemouseCallback的做用十分相似,只不过它容许你记住任何类型的变量(不仅是函数)。

用法

const memoizedValue = useMemo(() => valueNeededToBeMemoized, dependencies)
复制代码

useMemo接收一个函数,该函数的返回值就是须要被记住的变量,当useMemo的第二个参数dependencies数组里面的元素的值没有发生变化的时候,memoizedValue使用的就是上一次的值。下面是一个例子:

import React, { useMemo } from 'react'
import ReactDOM from 'react-dom'

const RenderPrimes = ({ iterations, multiplier }) => {
  const primes = React.useMemo(() => calculatePrimes(iterations, multiplier), [
    iterations,
    multiplier
  ])

  return (
    <div> Primes! {primes} </div>
  )
}

ReactDOM.render(<RenderPrimes />, document.getElementById('root')) 复制代码

上面的例子中calculatePrimes是用来计算素数的,所以每次调用它都须要消耗大量的计算资源。为了提升组件渲染的性能,咱们可使用useMemo来记住计算的结果,当iterationsmultiplier保持不变的时候,咱们就不须要从新执行calculatePrimes函数来从新计算了,直接使用上一次的结果便可。

注意事项

不是全部的变量要包裹在useMemo里面

useCallback相似,咱们只将那些确实有须要被记住的变量使用useMemo来封装,切记不能滥用useMemo,例以下面就是一个滥用useMemo的例子:

import React, { useMemo } from 'react'
import ReactDOM from 'react-dom'

const DummyDisplay = () => {
  const items = useMemo(() => ['1', '2', '3'], [])
  
  return (
    <> { items.map(item => <div key={item}>{item}</div>) } </> ) } ReactDOM.render(<DummyDisplay />, document.getElementById('root')) 复制代码

上面的例子中直接将items定义在组件外面会更好:

import React from 'react'
import ReactDOM from 'react-dom'

const items = ['1', '2', '3']

const DummyDisplay = () => {  
  return (
    <> { items.map(item => <div key={item}>{item}</div>) } </> ) } ReactDOM.render(<DummyDisplay />, document.getElementById('root')) 复制代码

useContext

做用

咱们知道React中组件之间传递参数的方式是props,假如咱们在父级组件中定义了某些状态,而这些状态须要在该组件深层次嵌套的子组件中被使用的话就须要将这些状态以props的形式层层传递,这就形成了props drilling的问题。为了解决这个问题,React容许咱们使用Context来在父级组件和底下任意层次的子组件之间传递状态。在Function Component中咱们可使用useContext Hook来使用context

用法

const value = useContext(MyContext)
复制代码

useContext接收一个context对象为参数,该context对象是由React.createContext函数生成的。useContext的返回值是当前context的值,这个值是由最邻近的<MyContext.Provider>来决定的。一旦在某个组件里面使用了useContext这就至关于该组件订阅了这个context的变化,当最近的<MyContext.Provider>context值发生变化时,使用到该context的子组件就会被触发重渲染,且它们会拿到context的最新值。下面是一个具体的例子:

import React, { useContext, useState } from 'react'
import ReactDOM from 'react-dom'

// define context
const NumberContext = React.createContext()

const NumberDisplay = () => {
  const [currentNumber, setCurrentNumber] = useContext(NumberContext)

  const handleCurrentNumberChange = () => {
    setCurrentNumber(Math.floor(Math.random() * 100))
  }

  return (
    <>
      <div>Current number is: {currentNumber}</div>
      <button onClick={handleCurrentNumberChange}>Change current number</button>
    </>
  )
}

const ParentComponent = () => {
  const [currentNumber, setCurrentNumber] = useState({})

  return (
    <NumberContext.Provider value={[currentNumber, setCurrentNumber]}>
      <NumberDisplay />
    </NumberContext.Provider>
  )
}

ReactDOM.render(<ParentComponent />, document.getElementById('root'))
复制代码

注意事项

避免无用渲染

咱们在上面已经提到若是一个Function Component使用了useContext(SomeContext)的话它就订阅了这个SomeContext的变化,这样当SomeContext.Providervalue发生变化的时候,这个组件就会被从新渲染。这里有一个问题就是,咱们可能会把不少不一样的数据放在同一个context里面,而不一样的子组件可能只关心这个context的某一部分数据,当context里面的任意值发生变化的时候,不管这些组件用不用到这些数据它们都会被从新渲染,这可能会形成一些性能问题。下面是一个简单的例子:

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const AppContext = React.createContext()

const ChildrenComponent = () => {
  const [appContext] = useContext(AppContext)
  const theme = appContext.theme

  return (
    <div>
      <ExpensiveTree theme={theme} />
    </div>
  )
}

const App = () => {
  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})

  return (
    <AppContext.Provider value={[appContext, setAppContext]}>
      <ChildrenComponent />
    </AppContext.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
复制代码

在上面的例子中,ChildrenComponent只使用到了appContext的.theme属性,但是当appContext其它属性例如configuration被更新时,ChildrenComponent也会被从新渲染,而ChildrenComponent调用了一个十分耗费性能的ExpensiveTree组件,因此这些无用的渲染会影响到咱们页面的性能,解决上面这个问题的方法有下面三种:

拆分Context

这个方法是最被推荐的作法,和useState同样,咱们能够将不须要同时改变的context拆分红不一样的context,让它们的职责更加分明,这样子组件只会订阅那些它们须要订阅的context从而避免无用的重渲染。例如上面的代码能够改为这样:

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const ThemeContext = React.createContext()
const ConfigurationContext = React.createContext()

const ChildrenComponent = () => {
  const [themeContext] = useContext(ThemeContext)

  return (
    <div>
      <ExpensiveTree theme={themeContext} />
    </div>
  )
}

const App = () => {
  const [themeContext, setThemeContext] = useState({ color: 'red' })
  const [configurationContext, setConfigurationContext] = useState({ showTips: false })

  return (
    <ThemeContext.Provider value={[themeContext, setThemeContext]}>
      <ConfigurationContext.Provider value={[configurationContext, setConfigurationContext]}>
        <ChildrenComponent />
      </ConfigurationContext.Provider>
    </ThemeContext.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
复制代码
拆分你的组件,使用memo来优化消耗性能的组件

若是出于某些缘由你不能拆分context,你仍然能够经过将消耗性能的组件和父组件的其余部分分离开来,而且使用memo函数来优化消耗性能的组件。例如上面的代码能够改成:

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const AppContext = React.createContext()

const ExpensiveComponentWrapper = React.memo(({ theme }) => {
  return (
    <ExpensiveTree theme={theme} />
  )
})

const ChildrenComponent = () => {
  const [appContext] = useContext(AppContext)
  const theme = appContext.theme

  return (
    <div>
      <ExpensiveComponentWrapper theme={theme} />
    </div>
  )
}

const App = () => {
  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})

  return (
    <AppContext.Provider value={[appContext, setAppContext]}>
      <ChildrenComponent />
    </AppContext.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
复制代码
不拆分组件,也可使用useMemo来优化

固然咱们也能够不拆分组件使用useMemo来将上面的代码进行优化,代码以下:

import React, { useContext, useState, useMemo } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const AppContext = React.createContext()

const ChildrenComponent = () => {
  const [appContext] = useContext(AppContext)
  const theme = appContext.theme

  return useMemo(() => (
      <div>
        <ExpensiveTree theme={theme} />
      </div>
    ),
    [theme]
  )
}

const App = () => {
  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})

  return (
    <AppContext.Provider value={[appContext, setAppContext]}>
      <ChildrenComponent />
    </AppContext.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
复制代码

useReducer

做用

useReducer用最简单的话来讲就是容许咱们在Function Component里面像使用redux同样经过reduceraction来管理咱们组件状态的变换(state transition)。

用法

const [state, dispatch] = useReducer(reducer, initialArg, init?)
复制代码

useReduceruseState相似,都是用来管理组件状态的,只不过和useStatesetState不同的是,useReducer返回的dispatch函数是用来触发某些改变stateaction而不是直接设置state的值,至于不一样的action如何产生新的state的值则在reducer里面定义。useReducer接收的三个参数分别是:

  • reducer: 这是一个函数,它的签名是(currentState, action) => newState,从它的函数签名能够看出它会接收当前的state和当前dispatchaction为参数,而后返回下一个state,也就是说它负责状态转换(state transition)的工做。
  • initialArg:若是调用者没有提供第三个init参数,这个参数表明的是这个reducer的初始状态,若是init参数有被指定的话,initialArg会被做为参数传进init函数来生成初始状态。
  • init: 这是一个用来生成初始状态的函数,它的函数签名是(initialArg) => initialState,从它的函数签名能够看出它会接收useReducer的第二个参数initialArg做为参数,并生成一个初始状态initialState。 下面是useReducer的一个简单的例子:
import React, { useState, useReducer } from 'react'

let todoId = 1

const reducer = (currentState, action) => {
  switch(action.type) {
    case 'add':
      return [...currentState, {id: todoId++, text: action.text}]
    case 'delete':
      return currentState.filter(({ id }) => action.id !== id)
    default:
      throw new Error('Unsupported action type')
  }
}

const Todo = ({ id, text, onDelete }) => {
  return (
    <div> {text} <button onClick={() => onDelete(id)} > remove </button> </div>
  )
}

const App = () => {
  const [todos, dispatch] = useReducer(reducer, [])
  const [text, setText] = useState('')

  return (
    <>
      {
        todos.map(({ id, text }) => {
          return (
            <Todo
              text={text}
              key={id}
              id={id}
              onDelete={id => {
                dispatch({ type: 'delete', id })
              }}
            />
          )
        })
      }
      <input onChange={event => setText(event.target.value)} />
      <button
        onClick={() => {
          dispatch({ type: 'add', text })
          setText('')
        }}
      >
        add todo
      </button>
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
复制代码

注意事项

useReducer vs useState

useReduceruseState均可以用来管理组件的状态,它们之间最大的区别就是,useReducer将状态和状态的变化统一管理在reducer函数里面,这样对于一些复杂的状态管理会十分方便咱们debug,由于它对状态的改变是封闭的。而因为useState返回的setState能够直接在任意地方设置咱们状态的值,当咱们组件的状态转换逻辑十分复杂时,它将很难debug,由于它是开放的状态管理。整体的来讲,在useReduceruseState如何进行选择的问题上咱们能够参考如下这些原则:

  • 下列状况使用useState
    • state的值是JS原始数据类型(primitives),如number, stringboolean
    • state的转换逻辑十分简单
    • 组件内不一样的状态是没有关联的,它们可使用多个独立的useState来单独管理
  • 下列状况使用useReducer
    • state的值是object或者array
    • state的转换逻辑十分复杂, 须要使用reducer函数来统一管理
    • 组件内多个state互相关联,改变一个状态时也须要改变另一个,将他们放在同一个state内使用reducer来统一管理
    • 状态定义在父级组件,不过须要在深层次嵌套的子组件中使用和改变父组件的状态,能够同时使用useReduceruseContext两个hook,将dispatch方法放进context里面来避免组件的props drilling
    • 若是你但愿你的状态管理是可预测的(predictable)和可维护的(maintainable),请useReducer
    • 若是你但愿你的状态变化能够被测试,请使用useReducer

自定义Hook

上面介绍了React内置的经常使用Hook的用法,接着咱们看一下如何编写咱们本身的Hook。

做用

自定义Hook的目的是让咱们封装一些能够在不一样组件之间共用的非UI逻辑来提升咱们开发业务代码的效率。

什么是自定义Hook

以前咱们说过Hook其实就是一个函数,因此自定义Hook也是一个函数,只不过它在内部使用了React的内置Hook或者其它的自定义Hook。虽然咱们能够任意命名咱们的自定义Hook,但是为了另其它开发者更容易理解咱们的代码以及方便一些开发工具例如eslint-plugin-react-hooks来给咱们更好地提示,咱们须要将咱们的Hook以use做为开头,而且使用驼峰发进行命名,例如useLocationuseLocalStorageuseQueryString等等。

例子

下面举一个最简单的自定义hook的例子:

import React, { useState, useCallback } from 'react'
import ReactDOM from 'react-dom'

const useCounter = () => {
  const [counter, setCounter] = useState(0)
  
  const increase = useCallback(() => setCounter(counter => ++counter), [])
  const decrease = useCallback(() => setCounter(counter => --counter), [])

  return {
    counter,
    increase,
    decrease
  }
}

const App = () => {
  const { counter, increase, decrease } = useCounter()

  return (
    <> <div>Counter: {counter}</div> <button onClick={increase}>increase</button> <button onClick={decrease}>decrease</button> </> ) } ReactDOM.render(<App />, document.getElementById('root')) 复制代码

总结

在本篇文章中我给你们介绍了React一些经常使用的内置Hook以及如何定义咱们本身的Hook。React Hook总的来讲是一个十分强大的功能,合理地使用它能够提升咱们代码的复用率和业务代码的开发效率,不过它也有不少隐藏的各式各样的坑,你们在使用中必定要多加防范,个人我的建议是你们尽可能使用eslint-plugin-react-hooks插件来辅助开发,由于它真的能够在咱们开发的过程当中就帮咱们发现代码存在的问题,不过有时候千方百计来去掉它的警告确实是很烦人的:)。

在这个系列的下一篇文章中我将教你们如何测试咱们自定义的Hook来提升咱们的代码质量,你们敬请期待。

参考文献

我的技术动态

文章始发于个人我的博客

欢迎关注公众号进击的大葱一块儿学习成长

相关文章
相关标签/搜索