美文网首页让前端飞React.js
使用React Hooks封装一套异步拉取的TableList

使用React Hooks封装一套异步拉取的TableList

作者: 换昵称了 | 来源:发表于2019-02-02 13:52 被阅读6次
    ps:想要获得更好的阅读体验,请访问原地址:(https://www.yuque.com/zibuyu/fed/ognwv0),该作者是个能力很强的前端开发。文章转载已获得许可。

    React在16.7-alpha版本中新增了React的一些新特性,如Hook。

    在这里主要想分享一下关于Hook使用

    About Hook

    PS: 了解上面三个 api 就可以对Hook进行基本的使用了。 类似于官方的声明可以直接去官网查看。下面直接进入实战模式。

    Practice

    这里我们先说明我们要做的是什么, 我们在这里最终目的是要做一个由Hook制作的一个异步Table组件,Table使用的是 **Ant-design **里封装好的组件, 而cli用 **create-react-app **官方自带吧。因为侧重 hook 所以我们不会涉及到redux之类的,所以也不必要徒增不必要的代码量。 then let's do it。

    Mock Data

    第一步,首先我们要构造后台的数据,或者自己找一个可以进行mock的数据就可以。这个尽可能模仿后台,我这里使用的就用 ant-design-pro 提供的分页。https://preview.pro.ant.design/api/rule?currentPage=1&pageSize=10 可以在postman搜索即可 ,浏览器打开不正常。估计是做了额外的处理。但是有个问题就是跨域问题解决不了,我这边基于easy-mock又做了一个 接口, 理论上两个行为是一致的。

    主要后台和前端配合下 前端传 当前页码 (currentPage) 和 展示数量 (pageSize) 即可。 后台返回需要有 总页数。 因为我们要做如下这个情况就必须得要有总页数。

    Like this. ↓

    image

    anyway. 数据有了就可以下一步了。

    Font-end

    构建目录

    • create-react-app 构建出基本的react项目 这个可以看官方文档

    • 对package.json进行修改,把 react , react-dom 两个修改成 v16.8.0-alpha.1, 然后重新安装

    • 这里还需要把 antd 加上 毕竟我们也要用到他们的Table组件

    • 还有一个请求这里直接用原生的fetch就可以了

    贴一下目前的package.json

    <pre class="cm-s-default" style="box-sizing: border-box; font-family: inherit; font-size: inherit; margin: 0px; overflow: visible; padding: 0px; border-radius: 0px; border-width: 0px; background: rgba(0, 0, 0, 0); white-space: pre; overflow-wrap: normal; line-height: inherit; color: inherit; z-index: 2; position: relative; -webkit-tap-highlight-color: transparent; font-variant-ligatures: contextual;">{
      "name": "my-app",
      "version": "0.1.0",
      "private": true,
      "dependencies": {
        "antd": "^3.13.0",
        "react": "v16.8.0-alpha.1",
        "react-dom": "v16.8.0-alpha.1",
        "react-scripts": "2.1.3"
      },
      "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test",
        "eject": "react-scripts eject"
      },
      "eslintConfig": {
        "extends": "react-app"
      },
      "browserslist": [
        ">0.2%",
        "not dead",
        "not ie <= 11",
        "not op_mini all"
      ]
    }</pre>
    

    接下来我们删除目前不需要用到的页面,并且把 server-worker 去掉, 最后简化到如下即可。 代码可在github上预览。

    接下来第一次尝试

    异步获取数据并且渲染到页面

    首先这里有一个关键的问题 , **异步 **在一开始,我们就得明确hook中使用带有副作用的都得使用 useEffect() 进行定义, 异步也是副作用的一种。因此,我们获取数据的时候应该是使用 useEffect() 代码走一个

    yap,我们可以得到我们想要的数据了!

    image

    解释一下这里, useEffect() 这个非常强大, 哈哈哈。 不愧是在React发布会上Ryan特别点了一下,真好用。哈哈

    这里主要是容纳了 componentDidMount 和 componentDidUpdate return 的时候就是 组件 umount的时机, 因此 subscribe在useEffect中,但return的时候就应该把他们给 unsubscribe。

    Ok. just fine.

    下一步。

    我们已经知道如何得到数据 , 那么我们把它塞到我们的Table组件中。试一下

    失败

    image

    这里错误的原因很简单,因为数据在更新之后并没有处罚需要 re-render 的条件,因此我们加上 useState()

    image

    数据请求成功。并且渲染成功! but,这里有一个问题。为什么我们会有这么多条请求?如果不报错甚至会出现死循环? 因为 调用的位置 出现问题了! 想想我们以前写的react, 只有一次的触发时机。 那就是 componentWillMount 或者 componentDidMount 那么 ,这个位置在哪里呢?

    useEffect() 就因此而来。

    image

    没有任何问题! 但是,这里我多做了一步,就是 给useEffect添加了第二个参数

    useEffect() 的第二个参数就是专门做数据对比,它的入参是一个变量。只有在变量指向的数据进行修改后,进行对比发现不一致时,才会重新进行渲染。

    好的,现在才是正题啊!

    封装异步Table组件

    定义入参方法

    展示的columns, 请求后台api, 传后台参数, 后台返回的数组的字键名称

    function asyncTableList(columns, queryAction, params, cgiKey) {

    // do something

    }

    整套代码如下

    <pre class="cm-s-default" style="box-sizing: border-box; font-family: inherit; font-size: inherit; margin: 0px; overflow: visible; padding: 0px; border-radius: 0px; border-width: 0px; background: rgba(0, 0, 0, 0); white-space: pre; overflow-wrap: normal; line-height: inherit; color: inherit; z-index: 2; position: relative; -webkit-tap-highlight-color: transparent; font-variant-ligatures: contextual;">/**
     * 异步table
     * 组件使用方法
     * 
     * import renderAsyncTable from './this.file'
     * 
     * const asyncTable = renderAsyncTable(展示的columns, 请求后台api, 传后台参数, 后台返回的数组的字键名称)
     * 
     * function App() {
     *   const [query] = useState()
     *   const columns = [...]
     *   const asyncTable = renderAsyncTable(columns, api, query, listName ) 
     *   return (
     *    <div>
     *      {asyncTable}
     *    </div>
     *   )
     * }
     */
    import React, { useEffect, useReducer } from 'react'
    import Table from 'antd/lib/table';
    
    function useAsyncTable (columns, queryAction, params, listName) {
      const paginationInitial = {
        current: 1,
        pageSize: 10
      }
      const [state, dispatch] = useReducer(
        (state, action) => {
          const { payload } = action
          switch (action.type) {
            case 'TOGGLE_LOADING':
              return { ...state, loading: !state.loading }
            case 'SET_QUERY':
              return {
                ...state,
                query: payload.params,
                pagination: paginationInitial
              }
            case 'SET_PAGINATION':
              return { ...state, pagination: payload.pagination }
            case 'SET_DATA_SOURCE':
              return { ...state, dataSource: payload.dataSource }
            default:
              return state
          }
        },
        {
          loading: false,
          query: null,
          pagination: paginationInitial,
          dataSource: []
        }
      )
      function handleTableChange (event) {
        if (event) {
          const { current } = event
          dispatch({
            type: 'SET_PAGINATION',
            payload: {
              pagination: {
                ...state.pagination,
                current
              }
            }
          })
        }
      }
      function doQuery () {
        dispatch({
          type: 'TOGGLE_LOADING'
        })
        const { current, pageSize } = state.pagination
        const pagination = {
          current,
          pageSize
        }
        queryAction({
          ...state.query,
          ...pagination
        })
          .catch(err => {
            dispatch({
              type: 'TOGGLE_LOADING'
            })
            return {}
          })
          .then((payload) => {
            if(payload.pagination && payload.list) {
              const { pagination: { total } } = payload
              console.log('total', total)
              dispatch({
                type: 'TOGGLE_LOADING'
              })
              if (!state.pagination.total) {
                dispatch({
                  type: 'SET_PAGINATION',
                  payload: {
                    pagination: { ...state.pagination, total }
                  }
                })
              }
              dispatch({
                type: 'SET_DATA_SOURCE',
                payload: {
                  dataSource: payload[listName]
                }
              })  
            }
    
          })
      }
      // cDM cDU
      useEffect(
        () => {
          if (params && JSON.stringify(params) !== JSON.stringify(state.query)) {
            dispatch({
              type: 'SET_QUERY',
              payload: {
                params
              }
            })
          } else {
            doQuery()
          }
        },
        [params, state.pagination.current, state.query]
      )
      return (
        <Table
          columns={columns}
          rowKey={record => record.key}
          pagination={state.pagination}
          dataSource={state.dataSource}
          loading={state.loading}
          onChange={handleTableChange}
        />
      )
    }
    export default useAsyncTable</pre>
    

    useReducer

    这里解释一下:

    在React官网的Hook中也有用到useReducer这种方法! 怎么说呢? 用hook的方式写一个redux吧。 用法和redux一样。state只能被dispatch的数据进行修改。 dispatch用type来区分修改state里的哪个数据,然后 我这边用payload 带数据进去。修改 state 内部数据。

    每次做diff的之后 只拿 透传过来的 params 和 存在reducer里的 query做对比,如果有修改就进行修改。

    没有的话就是调用请求。

    这里调用请求可以理解为 :

    1. 只有在 current 页面不一致的时候调用
    2. 只有在query更改过后进行修改

    而params在这里只是作为一个触发修改query的操作,并不会直接进行请求。

    调用页面

    调用相对简单。 我们需要传入的参数只有4个

    • columns的展示
    • 异步拉取的接口
    • 其他query, 这个query可能需要解析一下, 因为不是所有的异步拉取只有 pageSize 和 current 两个参数。 它们还有可能是 cityId 为 10000 或者 name是小明 也有可能是集合 如 城市A区,名字小白。这样,我们异步在使用异步拉取的接口 就可以不单单的延申为 pageSize 和 current这么单调。 它也可以combine参数后得到更好的拓展。
    • listName。 这个主要是为了不同的后台,有可能它是比较泛的名称如 lists 或者 beanList 等等,但也有可能是 cities,topics。 因此也用透传的方式。 更加强壮。
    <pre class="cm-s-default" style="box-sizing: border-box; font-family: inherit; font-size: inherit; margin: 0px; overflow: visible; padding: 0px; border-radius: 0px; border-width: 0px; background: rgba(0, 0, 0, 0); white-space: pre; overflow-wrap: normal; line-height: inherit; color: inherit; z-index: 2; position: relative; -webkit-tap-highlight-color: transparent; font-variant-ligatures: contextual;">import React, {useState} from 'react';
    
    import renderAsyncTable from './asyncTable'
    import getAjax from './utils/ajax'
    
    function App() {
      const [query] = useState()
    
      const columns = [
        {
          title: '规则名称',
          dataIndex: 'name',
        },
        {
          title: '描述',
          dataIndex: 'desc',
        },
        {
          title: '服务调用次数',
          dataIndex: 'callNo',
          render: val => `${val} 万`,
          // mark to display a total number
          needTotal: true,
        },
        {
          title: '上次调度时间',
          dataIndex: 'updatedAt',
        },
      ];
    
      const asyncTable = renderAsyncTable(columns, (query) => getAjax('o2po/data', query), query, 'list' ) 
    
      return (
        <div style={{width: 600, margin: '30px auto'}}>
          {asyncTable}
        </div>
      )
    }
    export default App;
    </pre>
    

    整套流程就可以了。

    image

    相关文章

      网友评论

        本文标题:使用React Hooks封装一套异步拉取的TableList

        本文链接:https://www.haomeiwen.com/subject/scyssqtx.html