react

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

2019-01-27  本文已影响0人  As大大

Background

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

构建目录

贴一下目前的package.json

代码块

{
  "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"
  ]
}

接下来我们删除目前不需要用到的页面,并且把 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

}

整套代码如下

代码块


/**
 * 异步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

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个

代码块

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;

整套流程就可以了。

调用页面

​github:
https://github.com/panmouren/asyncTableList

欢迎start,如果有什么问题欢迎指正。! 指正比start更重要。 语雀https://www.yuque.com/zibuyu/fed/ognwv0#Background

上一篇 下一篇

猜你喜欢

热点阅读