在React为何须要Hook这篇文章中咱们探讨了React开发团队为何要为Function Component添加Hook的缘由,在本篇文章中我将会为你们提供一份较为全面的React Hook实践指南,其中包括如下方面的内容:javascript
React Hook是React 16.8版本以后添加的新属性,用最简单的话来讲,React Hook就是一些React提供的内置函数,这些函数可让Function Component和Class Component同样可以拥有组件状态(state)以及进行反作用(side effect)。html
接下来我将会为你们介绍一些经常使用的Hook,对于每个Hook,我都会覆盖如下方面的内容:java
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
的函数。这里要注意的是state
和setState
这两个变量的命名不是固定的,应该根据你业务的实际状况选择不一样的名字,能够是text
和setText
,也能够是width
和setWidth
这类的命名。(对上面数组解构赋值不熟悉的同窗能够看下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相似,setCounter
和setText
均可以接收一个函数为参数,这个函数叫作updater
,updater
接收的参数是当前状态的最新值,返回值是下一个状态。例如setCounter的参数能够改为一个函数:github
<button
onClick={() => {
setCounter(counter => counter + 1)
}}
>
Increase counter
</button>
复制代码
useState
的initialState
也能够是一个用来生成状态初始值的函数,这种作法主要是避免组件每次渲染的时候initialState
须要被重复计算。下面是个简单的例子:express
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props)
return initialState
})
复制代码
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来管理你的状态,这样你的代码会更好维护。
若是setState接收到的新的state
和当前的state
是同样的(判断方法是Object.is),React将不会从新渲染子组件或者触发side effect
。这里要注意的是虽然React不会渲染子组件,不过它仍是会从新渲染当前的组件的,若是你的组件渲染有些很耗性能的计算的话,能够考虑使用useMemo来优化性能。
不管是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
是用来使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
基本能够覆盖componentDidMount
, componentDidUpdate
,componentWillUnmount
等生命周期函数组合起来使用的全部场景,可是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
的值,从而使组件重渲染,而重渲染后useEffect
的effect
继续被执行,进而组件再次重渲染。。。为了不重复的反作用执行,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-hooks的exhaustive-deps规则来进行编码约束,在你的项目加上这个约束以后,在代码开发阶段eslint就会告诉你要将someState放到useEffect
的dependencies
中去,这样就能够不使用useRef
来存储someState的值了,例以下面代码:
const [someState, setSomeState] = useState()
useEffect(() => {
...
console.log(someState)
}, [otherDependencies..., someState])
复制代码
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}
对象,只不过它能够保证在组件每次渲染的时候拿到的都是同一个对象。
useRef
返回的ref object被从新赋值的时候不会引发组件的重渲染,若是你有这个需求的话请使用useState
来存储数据。
随着Hook的出现,开发者开始愈来愈多地使用Function Component来开发需求。当开发者在定义Function Component的时候每每须要在函数体内定义一些内嵌函数(inline function),这些内嵌函数会在组件每次从新渲染的时候被从新定义,若是它们做为props传递给了子组件的话,即便其它props的值没有发生变化,它都会使子组件从新渲染,而无用的组件重渲染可能会产生一些性能问题。每次从新生成新的内嵌函数还有另一个问题就是当咱们把内嵌函数做为dependency
传进useEffect
的dependencies
数组的话,由于该函数频繁被从新生成,因此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
才是个空数组,若是你的函数有使用到外面的依赖的话,记得必定要将该依赖放进useCallback
的dependencies
参数中,否则会有bug发生。
和useEffect
相似,咱们也须要将全部在useCallback
的callback中使用到的外部变量写到dependencies
数组里面,否则咱们可能会在callback
调用的时候使用到“旧的”外部变量的值。
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
和useCallback
的做用十分相似,只不过它容许你记住
任何类型的变量(不仅是函数)。
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
来记住计算的结果,当iterations
和multiplier
保持不变的时候,咱们就不须要从新执行calculatePrimes函数来从新计算了,直接使用上一次的结果便可。
和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')) 复制代码
咱们知道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.Provider
的value
发生变化的时候,这个组件就会被从新渲染。这里有一个问题就是,咱们可能会把不少不一样的数据放在同一个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组件,因此这些无用的渲染会影响到咱们页面的性能,解决上面这个问题的方法有下面三种:
这个方法是最被推荐的作法,和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'))
复制代码
若是出于某些缘由你不能拆分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
来将上面的代码进行优化,代码以下:
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
用最简单的话来讲就是容许咱们在Function Component里面像使用redux同样经过reducer
和action
来管理咱们组件状态的变换(state transition)。
const [state, dispatch] = useReducer(reducer, initialArg, init?)
复制代码
useReducer
和useState
相似,都是用来管理组件状态的,只不过和useState
的setState
不同的是,useReducer
返回的dispatch
函数是用来触发某些改变state
的action
而不是直接设置state
的值,至于不一样的action
如何产生新的state的值则在reducer
里面定义。useReducer
接收的三个参数分别是:
(currentState, action) => newState
,从它的函数签名能够看出它会接收当前的state和当前dispatch
的action
为参数,而后返回下一个state,也就是说它负责状态转换(state transition)的工做。init
参数,这个参数表明的是这个reducer
的初始状态,若是init
参数有被指定的话,initialArg
会被做为参数传进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
和useState
均可以用来管理组件的状态,它们之间最大的区别就是,useReducer
将状态和状态的变化统一管理在reducer
函数里面,这样对于一些复杂的状态管理会十分方便咱们debug,由于它对状态的改变是封闭的
。而因为useState
返回的setState
能够直接在任意地方设置咱们状态的值,当咱们组件的状态转换逻辑十分复杂时,它将很难debug,由于它是开放的
状态管理。整体的来讲,在useReducer
和useState
如何进行选择的问题上咱们能够参考如下这些原则:
useState
state
的值是JS原始数据类型(primitives),如number
, string
和boolean
等state
的转换逻辑十分简单useState
来单独管理useReducer
state
的值是object
或者array
state
的转换逻辑十分复杂, 须要使用reducer
函数来统一管理state
互相关联,改变一个状态时也须要改变另一个,将他们放在同一个state
内使用reducer来统一管理useReducer
和useContext
两个hook,将dispatch
方法放进context里面来避免组件的props drilling
useReducer
useReducer
上面介绍了React内置的经常使用Hook的用法,接着咱们看一下如何编写咱们本身的Hook。
自定义Hook的目的是让咱们封装一些能够在不一样组件之间共用的非UI逻辑来提升咱们开发业务代码的效率。
以前咱们说过Hook其实就是一个函数,因此自定义Hook也是一个函数,只不过它在内部使用了React的内置Hook或者其它的自定义Hook
。虽然咱们能够任意命名咱们的自定义Hook,但是为了另其它开发者更容易理解咱们的代码以及方便一些开发工具例如eslint-plugin-react-hooks
来给咱们更好地提示,咱们须要将咱们的Hook以use
做为开头,而且使用驼峰发进行命名,例如useLocation
,useLocalStorage
和useQueryString
等等。
下面举一个最简单的自定义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来提升咱们的代码质量,你们敬请期待。
文章始发于个人我的博客
欢迎关注公众号进击的大葱一块儿学习成长