自从 React 16.8 版本正式发布 React Hooks 以来已通过去一个多月了,而在这以前国内外对于 Hooks API 的讨论也一直是如火如荼地进行着。有些人以为 Hooks API 很好用,而有些人却对它感到十分困惑。但 Dan Abramov 说过,就像 React 在 2013 年刚出来的时候同样,Hooks API 也须要时间被开发者们接受和理解。为了加深本身对 React Hooks 的认识,因而便有了将相关资料整理成文的想法。本文主要是记录本身在学习 React Hooks 时认为比较重要的点和常见的坑,固然也会记录相关的最佳实践以便本身更加熟练地掌握此种 mental model ( 心智模型 ) 。若是你还不了解 React Hooks ,请先移步到官方文档学习。html
组件中的每次 render 都有其特定且独立的 props 和 state ( 能够把每一次 render 看做是函数组件的再次调用 ),若是组件中含有定时器、事件处理器、其余的 API 甚至 useEffect ,因为闭包的特性,在它们内部的代码都会当即捕获当前 render 的 props 和 state ,而不是最新的 props 和 state 。react
让咱们先来看一个最简单的例子,而后你就可以马上理解上面那段话的意思了。ios
// 先触发 handleAlertClick 事件
// 而后在 3 秒内增长 count 至 5
// 最后 alert 的结果仍为 0
function Counter() {
const [count, setCount] = useState(0)
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count)
}, 3000)
}
// 最后的 document.title 为 5
useEffect(
() => {
document.title = `You clicked ${count} times`
}
)
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> <button onClick={handleAlertClick}> Show alert </button> </div>
)
}
复制代码
虽然最后 alert 的结果为 0 ,但咱们会发现最后的 document.title
倒是 5 。了解 Hooks API 的人都知道,这是由于 useEffect 中的 effect 函数会在组件挂载和每次组件更新以后进行调用,因此咱们获取到的 count 是最后一次 render 的 state ,它的值为 5 。若是咱们给 useEffect 加上第二个参数 []
,那最后咱们的 document.title
就会是 0 ,这是由于此时的 useEffect 不依赖任何值,因此相应的 effect 函数只会在组件挂载的时候被调用一次。说了这么多,不如给一张图解释的清楚,下面的图完美诠释了 useEffect 与 React Hooks 生命周期的联系。git
从这张图中咱们能够清楚地看到,每次 effect 函数调用以前都会先调用 cleanup 函数,并且 cleanup 函数只会在组件更新和组件卸载的时候调用,那么这个 cleanup 函数有什么做用呢?让咱们来看一段代码。github
useEffect(() => {
let didCancel = false
const fetchData = async () => {
const result = await axios(url)
if (!didCancel) {
setData(result.data)
}
}
fetchData()
// 这里 return 的即是咱们的 cleanup 函数
return () => {
didCancel = true
}
}, [url])
复制代码
这段代码解决了在网络请求中常见的竞态问题。假设咱们没有调用 cleanup 函数,当咱们连续调用两次 effect 函数时,因为请求数据到达时间的不肯定,若是第一次请求的数据后到达,虽然咱们想在浏览器屏幕上呈现的是第二次请求的数据,但结果却只会是第一次请求的数据。再一次的,因为闭包的特性,当咱们执行 didCancel = true
时,在前一次的 effect 函数中 setData(result)
就没法被执行,竞态问题也便迎刃而解。固然 cleanup 函数还有不少常见的应用场景,例如清理定时器、订阅源等。npm
上面那张图还有几个值得咱们注意的点:redux
而后咱们来说下 useEffect 的第二个参数:axios
它用于跟前一次 render 传入的 deps ( 依赖 ) 进行比较,为的是避免没必要要的 effect 函数再次执行。useEffect 的运行机制应该是先比较 deps ,如有不一样则执行先前的 cleanup 函数,而后再执行最新的 effect 函数,若相同则跳过上面的两个步骤。若是要用函数做为 useEffect 的第二个参数,则须要使用 useCallback ,其做用是为了不该函数在组件更新时再次被建立,从而使 useEffect 第二个参数的做用失效。api
在这里个人理解是因为两个同名函数比较时总会返回 false ,并且使用 useCallback 也须要第二个参数,所以我猜想 React 最终仍是以值的比较来达到“缓存”函数的效果。数组
var a = function foo () {}
var b = function foo () {}
a === b // false
复制代码
为了方便理解,下面是一个使用 useCallback 的例子。
// 使用 useCallback,并将其传递给子组件
function Parent() {
const [query, setQuery] = useState('react')
// 只有当 query 改变时,fetchData 才会发生改变
const fetchData = useCallback(() => {
const url = 'https://hn.algolia.com/api/v1/search?query=' + query
}, [query])
return <Child fetchData={fetchData} /> } function Child({ fetchData }) { let [data, setData] = useState(null) useEffect(() => { fetchData().then(setData) }, [fetchData]) } 复制代码
最后咱们要实现的功能:
下面是以三种不一样的方式实现的例子。
function App() {
const [data, setData] = useState({ hits: [] })
const [query, setQuery] = useState('redux')
const [url, setUrl] = useState(
'http://hn.algolia.com/api/v1/search?query=redux'
)
const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false)
useEffect(() => {
let didCancel = false
const fetchData = async () => {
setIsError(false)
setIsLoading(true)
try {
const result = await axios(url)
if (!didCancel) {
setData(result.data)
}
} catch (error) {
if (!didCancel) {
setIsError(true)
}
}
setIsLoading(false)
}
fetchData()
return () => {
didCanel = true
}
}, [url])
return (
<>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="button"
onClick={() =>
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
}
>
Search
</button>
{isError && <div>Something went wrong ...</div>}
{isLoading ? (
<div>Loading ...</div>
) : (
<ul>
{data.hits.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</>
)
}
复制代码
const useDataApi = (initialUrl, initialData) => {
const [data, setData] = useState(initialData)
const [url, setUrl] = useState(initialUrl)
const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false)
useEffect(() => {
let didCancel = false
const fetchData = async () => {
setIsError(false)
setIsLoading(true)
try {
const result = await axios(url)
if (!didCancel) {
setData(result.data)
}
} catch (error) {
if (!didCancel) {
setIsError(true)
}
}
setIsLoading(false)
}
fetchData()
return () => {
didCanel = true
}
}, [url])
const doFetch = url => {
setUrl(url)
}
return { data, isLoading, isError, doFetch }
}
function App() {
const [query, setQuery] = useState('redux')
const { data, isLoading, isError, doFetch } = useDataApi(
'http://hn.algolia.com/api/v1/search?query=redux',
{ hits: [] }
)
return (
<>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="button"
onClick={() =>
doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`)
}
>
Search
</button>
{isError && <div>Something went wrong ...</div>}
{isLoading ? (
<div>Loading ...</div>
) : (
<ul>
{data.hits.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</>
)
}
复制代码
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false
}
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: action.payload,
}
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
}
default:
throw new Error()
}
}
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl)
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
})
useEffect(() => {
let didCancel = false
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' })
try {
const result = await axios(url)
if (!didCancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: result.data })
}
} catch (error) {
if (!didCancel) {
dispatch({ type: 'FETCH_FAILURE' })
}
}
}
fetchData()
return () => {
didCancel = true
}
}, [url])
const doFetch = url => {
setUrl(url)
}
return { ...state, doFetch }
}
复制代码
组件挂载时调用
const onMount = () => {
// ...
}
useEffect(() => {
onMount()
}, [])
复制代码
组件卸载时调用
const onUnmount = () => {
// ...
}
useEffect(() => {
return () => onUnmount()
}, [])
复制代码
获取组件最新的 state
function Message() {
const [message, setMessage] = useState('')
const latestMessage = useRef('')
useEffect(() => {
latestMessage.current = message
}, [message])
const showMessage = () => {
alert('You said: ' + latestMessage.current)
}
const handleSendClick = () => {
setTimeout(showMessage, 3000)
}
const handleMessageChange = (e) => {
setMessage(e.target.value)
}
return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
)
}
复制代码
获取组件前一次的 state
function Counter() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return <h1>Now: {count}, before: {prevCount}</h1>
}
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
复制代码
function Parent({ a, b }) {
const child1 = useMemo(() => <Child1 a={a} />, [a])
const child2 = useMemo(() => <Child2 b={b} />, [b])
return (
<> {child1} {child2} </> ) } 复制代码
function ParentInput() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus()
}, [])
return (
<div>
<ChildInput ref={inputRef} />
</div>
)
}
function ChildInput(props, ref) {
const inputRef = useRef(null)
useImperativeHandle(ref, () => inputRef.current)
return <input type="text" name="child input" ref={inputRef} />
}
复制代码
借助 Hooks 和 Context 咱们能够轻松地实现状态管理,下面是我本身实现的一个简单状态管理工具,已发布到 npm 上,后续可能有大的改进,感兴趣的能够关注下 😊。
目前的源码只有几十行,因此给出的是 TS 的版本。
import * as React from 'react'
type ProviderProps = {
children: React.ReactNode
}
export default function createChrox (
reducer: (state: object, action: object) => object,
initialState: object
) {
const StateContext = React.createContext<object>({})
const DispatchContext = React.createContext<React.Dispatch<object>>(() => {})
const Provider: React.FC<ProviderProps> = props => {
const [state, dispatch] = React.useReducer(reducer, initialState)
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>
{props.children}
</StateContext.Provider>
</DispatchContext.Provider>
)
}
const Context = {
state: StateContext,
dispatch: DispatchContext
}
return {
Context,
Provider
}
}
复制代码
下面是利用该状态管理工具实现的一个 counter 的例子。
// reducer.js
export const initialState = {
count: 0
}
export const countReducer = (state, action) => {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 }
case 'decrement':
return { ...state, count: state.count - 1 }
default:
return { ...state }
}
}
复制代码
入口文件 index.js
import React, { useContext } from 'react'
import { render } from 'react-dom'
import createChrox from 'chrox'
import { countReducer, initialState } from './reducer'
const { Context, Provider } = createChrox(countReducer, initialState)
const Status = () => {
const state = useContext(Context.state)
return (
<span>{state.count}</span>
)
}
const Decrement = () => {
const dispatch = useContext(Context.dispatch)
return (
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
)
}
const Increment = () => {
const dispatch = useContext(Context.dispatch)
return (
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
)
}
const App = () => (
<> <Decrement /> <Status /> <Increment /> </> ) render( <Provider> <App /> </Provider>, document.getElementById('root') ) 复制代码
从上面能够看到我是基于 useReducer + useContext 来实现的状态管理,至于为何要这样作,那是由于这样作有两个主要的好处:
dispatch
函数只会在组件挂载的时候初始化,而在以后的组件更新中并不会发生改变 ( 值得注意的是 useRef 也具备相同的特性 ) ,所以它至关于一种更好的 useCallback 。当遇到很深的组件树时,咱们能够经过两个不一样的 Context 将 useReducer 返回的 state
和 dispatch
分离,这样若是组件树底层的某个组件只须要 dispatch
函数而不须要 state
,那么当 dispatch
函数调用时该组件是不会被从新渲染的,由此咱们便达到了性能优化的效果。写完整篇文章,往回看发现 React Hooks 确实是一种独特的 mental model ,凭借着这种“可玩性”极高的模式,我相信开发者们确定能探索出更多的最佳实践。不得不说 2019 年是 React 团队带给开发者惊喜最多的一年,由于仅仅是到今年中期,React 团队就会发布 Suspense、React Hooks、Concurrent Mode 三个重要的 API ,而这一目标早已实现了一半。也正是由于这个富有创造力的团队,让我今生无悔入 React 😂。
参考内容:
Making setInterval Declarative with React Hooks