当前位置: 首页 > 工具软件 > use-http > 使用案例 >

React Hook中useCallback及http封装及其最佳实践

寇桐
2023-12-01

前言

最近开始学习React,跟着Kent学,有很多干货,这里分享React Hook中的useCallback

代码分享到了codesandbox,App.js是最后的最佳实践,中间代码放在了archive文件夹上了


一、background

正常来说,当处理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])

二、应用

2.1 原型

这里还是通过pokemon的例子,这里例子在讲React Hook处理http请求也用过,这里同样适用于useCallback的应用,原型可以参考这里

2.2 需求场景

用户每次输入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
}

2.2 演进 - 解决dependency list的问题

上面的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;
};

2.2 演进 - 解决组件需要写asyncCallback

上面的方案是提高了性能,但是问题是每次使用组件的时候,都需要记得写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

 类似资料: