距离上一篇讨论 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 这种把异步数据放在 HOC 自动处理的解决方法。顺便说一下,Apollo 新版 API 推荐的 renderProps 明显更加难用了... 当然 hooks 的版本也已经 beta 了。但当把逻辑彻底放在 HOC 或者 renderProps 里时,如果一个组件依赖于多个请求并且它们之间又有纠缠时,不是特别方便处理。这是因为 HOC 或 renderProps 莫名的建立起了一个本不存在的层级和作用域关系。

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

useData ?

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,以及这个数据源依的请求参数 pagesize。这样看似没问题,但实际上 useEffect 的 dependencies [dataLoader, ...params] 无法满足 hook 要求的静态依赖,因为 useData 内部无法判断 ...params 里具体有哪些值。上一章我说 useCallback 一般用于性能,没有必要使用。这里就可以看出来其实说错了...

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 中。这里以竞态的问题为例,大致介绍一下可能的方法:

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 只能解决关于数据获取的那一部分问题,为了处理我们得到的几个状态,不免需要写出这样的代码:

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:

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 里完成所有的事情呢?这里再抛出一个彩蛋:

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 值得一试~

缺少必要的请求参数: id_React Hooks 第二期:发请求这件小事相关推荐

  1. Android添加Header请求参数实例,java响应header请求实现demo

    1.首先添加AsyncHttpClient.jar包到libs文件夹下 2.初始化请求类以及响应回调类 private AsyncHttpClient client; private AsyncHtt ...

  2. python编写请求参数带文件_python requests 库请求带有文件参数的接口实例

    有些接口参数是一个文件格式,比如fiddler 抓包参数如下显示 这个接口的 form-data fiddler 显示的和不带文件参数的接口有明显区别,显示的不是简单的键值对,所以我们也不能只通过 d ...

  3. 获取请求参数通用方式|| 中文乱码问题||请求转发

    1. 获取请求参数通用方式:不论get还是post请求方式都可以使用下列方法来获取请求参数 1. String getParameter(String name):根据参数名称获取参数值    use ...

  4. gin ajax 获取请求参数,go的gin框架从请求中获取参数的方法

    前言: go语言的gin框架go里面比较好的一个web框架, github的start数超过了18000.可见此框架的可信度 如何获取请求中的参数 假如有这么一个请求: POST   /post/te ...

  5. Java请求参数检查,Java如何检查servlet请求中是否存在参数?

    ServletRequest或HttpServletRequest对象具有用于映射参数名称及其值的映射对象.通过访问此映射,我们可以检查servlet请求中是否传递了参数.让我们看下面的例子.pack ...

  6. android volley 请求参数,android – Volley – 如何发送DELETE请求参数?

    同样的问题在这里,但我找到了解决方案 问题是在com.android.volley.toolbox.HttpClientStack.java中实现createHttpRequest方法,只有在请求方法 ...

  7. 获取referer中的请求参数_javaweb之request获取referer请求头实现防盗链

    在开发web程序的时候,有时我们需要得到用户是从什么页面连过来的,这就用到了referer. 它是http协议,所以任何能开发web程序的语言都可以实现,比如jsp中是: request.getHea ...

  8. php接收不到ajax请求参数,我是否需要在ajax请求和接收该请求的php之间编码/解码查询参数?...

    人们,如果你不是绝对确定你在做什么,请不要使用这个代码 此代码受到灾难性安全漏洞的影响,因此除非您绝对确定没有邪恶的人会访问它,否则请不要使用它. 发送: jQuery.ajax({ type: 'p ...

  9. html action get post请求参数乱码,input type=text 发送请求参数,中文乱码问题

    1.不废话,直接代码如下: teacher.jsp pageEncoding="UTF-8"%> 教师管理页面 课程资源目录: --------------- 作业提交目录: ...

最新文章

  1. 蚂蚁金服蓝绿发布实践
  2. LibTorch NMS
  3. ajax可以在localhost上用吗_你还不知道跨域问题是怎样造成的吗?
  4. [转载]Office Visio快捷键
  5. EOS账户系统(8)密钥被盗恢复
  6. 软件项目管理0718:读一页项目管理读书笔记
  7. python类及其方法
  8. 云上自动化:云上编排让上云更简单
  9. 如何提升你的javascript代码逼格之简写篇
  10. 电流电压曲线 vc源码_电瓶修复—充电曲线你知道多少?
  11. C++的类型转换操作符
  12. 诺基亚n1系统更新显示无网络_曾经世界第一大手机系统,诺基亚塞班系统竟还活着!你用过吗?...
  13. java red5 流媒体服务_[Red5]Red5之Flash流媒体服务器的安装与使用教程完整版(组图)...
  14. java如何获取全部省市_纯java获取省市区
  15. 用PS将照片背景变成白色
  16. 最新个人所得税计算方法
  17. 微信小程序/小游戏运行环境小结
  18. 泼冷水!为什么说机器学习在很多方面被高估了? | 精选
  19. 微信公众平台开发——引言
  20. UE Gameplay实例49(高级蒙太奇动画连招)

热门文章

  1. Linux IPC实践(11) --System V信号量(1)
  2. Linux信号实践(4) --可靠信号
  3. apache配置多个站点
  4. Android studio Github 断开连接
  5. LeetCode:Remove Nth Node From End of List 移除链表倒第n项
  6. struts2和struts1认识
  7. HttpModule的认识与深入理解
  8. ios 给网页传值_iOS学习——页面的传值方式
  9. 数颜色(洛谷-P1903)
  10. 山峰和山谷(信息学奥赛一本通-T1454)