---
# System prepended metadata

title: 批注： useHook() 第二期：聊聊发请求这件小事

---


# 批注： useHook() 第二期：聊聊发请求这件小事

距离上一篇讨论 hooks 的文章已经过了半年了，因为越使用就越发现这个坑不浅。“这段代码写得不好，为什么要放 hooks 很难让人理解。”是我主管在 review 了我使用 hooks 的代码之后说的第一句话。就连发请求这件最简单的事情，我都很难讲清楚为什么要使用 hooks。关于 React Hooks 也看了不少文章，但是始终没找到关于"最简单"的 CRUD 日常实践。所以这期我就抛砖迎玉，说说我目前的粗浅理解。

## 问题

假设有个最简单的需求，`MovieList` 页面需要从服务端获取数据并展示。这里先抛出几个问题：

1. 请求的方法写在哪里？请求来的数据放在哪里？
2. 如果请求还未完成，组件已被 Unmount，有没有做特殊处理?
3. 请求的参数来自哪里，参数变化会触发重新请求吗？调用写在哪里？
4. 请求对应的几个状态: loading，refreshing，error 在组件上处理了么？
5. 如果请求需要轮询，怎么做？有没有处理页面 visibilitychange 的情况？
6. 如果需要在参数变化后重新请求，如果参数频繁更新，会出现竞态（旧的请求因为慢，晚于后发的请求 resolve）的问题吗？

这些都是我觉得在 Class Component 里写“过程式”代码较难解决的问题。有的问题即使使用了 redux 依然没法很方便的处理。所以之前我很喜欢 [Apollo](https://zhuanlan.zhihu.com/p/34238617) 这种把异步数据放在 HOC 自动处理的解决方法。顺便说一下，Apollo 新版 API 推荐的 renderProps 明显更加难用了... 当然 hooks 的版本也已经 beta 了。但当把逻辑彻底放在 HOC 或者 renderProps 里时，如果一个组件依赖于多个请求并且它们之间又有纠缠时，不是特别方便处理。这是因为 HOC 或 renderProps 莫名的建立起了一个本不存在的层级和作用域关系。

那么在 Hooks 里是怎么样的呢？

## useData ?

```jsx
function useData(dataLoader, params) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(undefined);
  const [data, setData] = useState(undefined);
  
  useEffect(() => {
    dataLoader().then((responseData) => {
      // ...
      setData(responseData);
    });
  }, [dataLoader, ...params);
  
  return { data, loading, error };
}

function MovieList({ page, size }) {
  const {
    data: movieListData,
    loading,
    error
  } = useData(
    () => api.queryMovieList({ page, size }),
    [page, size]
  );

  // ...
}
```

第一个想到的是这样一个 Hook，类似于 useEffect，传入获取请求数据的 `dataLoader`，以及这个数据源依的请求参数 `page` 和 `size`。这样看似没问题，但实际上 useEffect 的 dependencies `[dataLoader, ...params]` 无法满足 hook 要求的静态
依赖，因为 `useData` 内部无法判断 `params` 里具体有哪些值。上一章我说 useCallback 一般用于性能，没有必要使用。这里就可以看出来其实说错了...

```jsx
function useData(dataLoader) {
  // ...

  useEffect(() => {
    dataLoader().then((responseData) => {
      // ...
      setData(responseData);
    });
  }, [dataLoader);
  
  // ...
}


function MovieList({ page, size }}) {
  const queryMovieList = useCallback((page, size) => api.queryMovieList({ page, size }), [page, size]);
  
  const { data: movieListData, loading, error } = useData(queryMovieList);

  // ...
}
```

这里我们利用 useCallback 将 queryMovieList 和它的参数 page，size 绑定起来。只有在 page 或 size 变化时，queryMovieList 才会更新，然后自动触发 useData 内部的请求逻辑。这样的写法，虽然在组件里多了一次 useCallback 的调用，但是更利于 hook 的静态分析。

有了 `useData` 这样的封装，得益于 hooks 拥有的完整组件生命周期控制，我们可以很容易的将之前提到的组件卸载处理，请求竞态等等的问题，封装在 hooks 中。这里以竞态的问题为例，大致介绍一下可能的方法：

```jsx
function useData(dataLoader) {
  const currentDataLoader = useRef(null);
  // ...

  useEffect(() => {
    currentDataGetter.current = dataGetter;

    dataLoader().then((responseData) => {
      // ...

      // 如果有更新的请求，放弃之前的
      if (currentDataGetter.current !== dataGetter) {
        return;
      }

      setData(responseData);
    });
  }, [dataLoader);
  
  // ...
}
```
只需要利用 ref 记录下最新的一个 dataLoader，那么在请求 resolve 时就可以判断出是否是过时的数据了。当然以上的代码只是一个示例，还有更多可以调整优化的地方。

## 处理异步状态

以上的 useData 只能解决关于数据获取的那一部分问题，为了处理我们得到的几个状态，不免需要写出这样的代码：

```jsx
function MovieList({ page, size }}) {
  const queryMovieList = useCallback((page, size) => api.queryMovieList({ page, size }), [page, size]);
  
  const { data: movieListData, loading, error } = useData(queryMovieList);

  if (loading && data == null) {
    return <Spin />
  }
  
  if (error) {
    return <Exception />
  }
  
  return (
    <Spin loading={loading}>
      {renderMovieList(movieListData)}
    </Spin>
  );
  
  function renderMovieList(movieList) {
    return movieList.map(item => <MovieItem key={item.id} data={item} />)
  }
}
```

久而久之我们会发现在所有使用这个 useData 的组件中，我们都避免不了手写这两个 `if`。但是 hooks 只能解决生命周期的问题，没法封装一些 render 的逻辑。其实这里最有效的解决方法就是 Suspense，但是因为还没有发布，所以我们想到了 renderProps:

```jsx
function DataBoundary({ data, loading, error, children }) {
  if (loading) {
    return <Spin />;
  }
  
  // ...
  
  return <Spin loading={loading}>{children(data)}</Spin>;
}

function MovieList({ page, size }}) {
  const queryMovieList = useCallback((page, size) => api.queryMovieList({ page, size }), [page, size]);
  
  const movieListResult = useData(queryMovieList);
  // const { data: movieListData, loading, error } = useData(queryMovieList);

  return (
    <DataBoundary {...movieListResult}>
    // <DataBoundary data={movieListData} loading={loading} error={error}>
      {(data) => renderMovieList(data)}
    </DataBoundary>
  );
  
  function renderMovieList(movieList) {
    return movieList.map(item => <MovieItem key={item.id} data={item} />)
  }
}
```

在 `DataBoundary` 里，我们封装了关于异步状态处理的渲染流程，还可以提供出类似 fallback 的钩子，满足需要定制化的组件场景。用它做到了类似于 Suspense 的事情。

## All in Hooks？

但是如果我们真的想在 hooks 里完成所有的事情呢？这里再抛出一个彩蛋：

```jsx
function useDataBoundary(dataLoader) {
  // ...
  
  function boundary(renderChildren) {
    if (loading && data == null) {
      return <Spin />;
    }

    // ...

    return renderChildren(data);
  }
  
  return boundary;
}

function MovieList({ page, size }}) {
  const queryMovieList = useCallback((page, size) => api.queryMovieList({ page, size }), [page, size]);
  
  const boundary = useDataBoundary(queryMovieList);

  return (
    <div>
      {boundary((data) => renderMovieList(data))}
    </div>
  );
  
  function renderMovieList(movieList) {
    return movieList.map(item => <MovieItem key={item.id} data={item} />)
  }
}
```

如果我们可以在 hooks 中返回一个 "renderProp"，我们就可以完整的将 render 相关的逻辑也封装在 hooks 里了，但是同时这样也会使得一个 hook 里的代码变得复杂，这里只是提供一种思路。那么这两种写法你更喜欢那个呢？

## 尾巴

这篇文章大致说明了在使用 hooks 写业务逻辑时的一点思考。很多地方没有详细展开，大家如果有兴趣的话可以再深入讨论。至少在最开始提出的几个问题上，hooks 帮我解决了这些原本没有地方安置的抽象逻辑。所以虽然我还是经常遇到 deps 没考虑清楚导致死循环的请求，我依然觉得 hooks 值得一试~
