最近开始学习React,跟着Kent学,有很多干货,这里分享React Hook中的useCallback
代码分享到了codesandbox,App.js是最后的最佳实践,中间代码放在了archive文件夹上了
正常来说,当处理sideEffect的时候,useEffect的第二个参数会传一个dependence list,只有当list中地参数变化时候,这个effect才会被重新触发,比如
React.useEffect(() => {
window.localStorage.setItem('count', count)
}, [count])
这里只有当count变化的时候,才会setItem(‘count’, count)到localstorage
这里的count是变量,如果是函数的话,就没那么简单了
const updateLocalStorage = () => window.localStorage.setItem('count', count)
React.useEffect(() => {
updateLocalStorage()
}, [updateLocalStorage])
useEffect是定义在再component的函数体内的,一旦component被render,updateLocalStorage就会被重新initialised,所以dependency list中的updateLocalStorage肯定是会变化的,也就是useEffect每次都会因为component更新而重新执行
于是,useCallback就可以用上场了;updateLocalStorage 函数使用useCallback包裹,把count定义成它的dependency
const updateLocalStorage = React.useCallback(
() => window.localStorage.setItem('count', count),
[count],
)
React.useEffect(() => {
updateLocalStorage()
}, [updateLocalStorage])
这里还是通过pokemon的例子,这里例子在讲React Hook处理http请求也用过,这里同样适用于useCallback的应用,原型可以参考这里
用户每次输入pokemon的时候,就会出发http请求,并且我们把http请求的逻辑搬离在组件外面
之前,我们使用useEffect
const [state, dispatch] = React.useReducer(asyncReducer, {
status: pokemonName ? 'pending' : 'idle',
pokemon: null,
error: null,
})
React.useEffect(() => {
if (!pokemonName) {
return
}
dispatch({type: 'pending'})
fetchPokemon(pokemonName).then(
pokemon => {
dispatch({type: 'resolved', pokemon})
},
error => {
dispatch({type: 'rejected', error})
},
)
}, [pokemonName])
这里的pokemonInfoReducer其实就是统一处理http正status(idle, pending, resolved, rejected)的逻辑,实现如下
function asyncReducer(state, action) {
switch (action.type) {
case "pending": {
return { status: "pending", data: null, error: null };
}
case "resolved": {
return { status: "resolved", data: action.data, error: null };
}
case "rejected": {
return { status: "rejected", data: null, error: action.error };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
现在,如果我们需要把这段useEffect抽离,变成如下的API,将会增加http的复用逻辑,并且也做到一定的解耦。也就是说,我们通过useAsync(asyncCallback, initialState)返回{data, status, error}的state
const state = useAsync(
() => {
if (!pokemonName) {
return
}
return fetchPokemon(pokemonName)
},
{status: pokemonName ? 'pending' : 'idle'},
[pokemonName],
)
const {data: pokemon, status, error} = state
之后,useAsync的实现如下,只要传入需要请求的http函数,useReducer的initialState,以及dependency list,就可以返回{data, status, error}的state,这样就把useEffect的异步请求做了一次封装,之后类似的场景就可以使用了
function useAsync(asyncCallback, initialState, dependencies) {
const [state, dispatch] = React.useReducer(asyncReducer, {
status: 'idle',
data: null,
error: null,
...initialState,
})
React.useEffect(() => {
const promise = asyncCallback()
if (!promise) {
return
}
dispatch({type: 'pending'})
promise.then(
data => {
dispatch({type: 'resolved', data})
},
error => {
dispatch({type: 'rejected', error})
},
)
// eslint plugin 会报错
}, dependencies)
return state
}
上面的useAsync有个问题,就是传入的dependencies eslint是会报错的,我们需要解决这个问题。
我们本质上是希望asyncCallback这个函数只有在变化的时候才触发useEffect,那么,我们是不是可以把asyncCallback放在dependency list中呢?是不可以的,原因就是asyncCallback这个函数会在就会在每次useEffect被重新initialised,所以回到我们background中讲到的问题。React于是提出了useCallback来解决我们遇到的问题
具体来说我们需要把asyncCallback通过useCallback包裹起来,并且告诉React什么时候才会触发useCallback;那么,对于pokemon的例子,asyncCallback是在pokemonName改变的时候真正需要触发,那么asyncCallback的修改如下
const asyncCallback = React.useCallback(() => {
if (!pokemonName) {
return;
}
return fetchPokemon(pokemonName);
}, [pokemonName]);
有了上面的改进之后,我们就可以重新改一下之前的代码了
function PokemonInfo({ pokemonName }) {
const asyncCallback = React.useCallback(() => {
if (!pokemonName) {
return;
}
return fetchPokemon(pokemonName);
}, [pokemonName]);
const state = useAsync(asyncCallback, {
status: pokemonName ? "pending" : "idle"
});
const { data: pokemon, status, error } = state;
...
//处理render的逻辑
}
useAsync传入的参数也变少了,因为我们不再需要传入dependency list
const useAsync = (asyncCallback, init) => {
const [state, dispatch] = React.useReducer(pokemonInfoReducer, {
status: "idle",
data: null,
error: null,
...init
});
React.useEffect(() => {
const promise = asyncCallback();
if (!promise) {
return;
}
dispatch({ type: "pending" });
promise.then(
(data) => {
dispatch({ type: "resolved", data });
},
(error) => {
dispatch({ type: "rejected", error });
}
);
}, [asyncCallback]);
return state;
};
上面的方案是提高了性能,但是问题是每次使用组件的时候,都需要记得写asyncCallback才行,这个在团队中是比较难以做到统一的,我们需要让组件使用者不需要写asyncCallback,这样才能保证性能不受API使用者的影响;这样使用者的代码大致应该如下,useAsync向外提供run方法,这个方法接受一个promise然后负责执行,结果保存到data中
const { data: pokemon, status, error, run } = useAsync({
status: pokemonName ? "pending" : "idle"
});
React.useEffect(() => {
if (!pokemonName) {
return;
}
run(fetchPokemon(pokemonName));
}, [pokemonName, run]);
结合上面的使用需求,不妨把引入asyncCallback到useAsync function中,代码修改如下;
function useAsync(initialState) {
const [state, dispatch] = React.useReducer(pokemonInfoReducer, {
status: 'idle',
data: null,
error: null,
...initialState,
})
const run = React.useCallback((promise) => {
dispatch({type: 'pending'})
promise.then(
data => {
dispatch({type: 'resolved', data})
},
error => {
dispatch({type: 'rejected', error})
},
)
}, [])
return {...state, run}
}
至此,一个比较完整的useAsync解法方案也就完成了,使用者只需要调用useAsync,并提供reducer的initState就能比较高性能地调用http并且获取数据,做到了组件以及http请求的解耦
当然这个useAsync的封装也并不是完美的,有一个小bug,那就是dispatch是不安全的;具体来说就是如果用户在dispatch没玩完成之前就离开这个component就会有如下的warming
Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
解决方案就是需要实现一个useSafeDispatch函数,由于实现起来有点复杂,内容超出跟本文章讨论的范畴,这里就不提供实现了,大家感兴趣可以留言实现方案,我们可以讨论
这里比较细致讲了useCallback的使用场景,以及通过具体的例子演进了通过useCallback的http通用函数的封装;总的来说useCallback一般使用场景就是dependency list传入函数;在写代码的时候对于可以复用的代码尽量进行封装,并向外提供可靠简单的API