React Hooks实践--抽离组件中的可复用逻辑

6,028

先看需求:

系统中的若干个页面需要展示表格数据,其中某些表格数据过多需要分页查询,而某些不用。

组件划分:

components:
    TableDisplay.jsx 
    Pagination.jsx
containers:
    TableContainer.jsx

TableDisplay:表格数据展示组件,参数如下:

  • data: Array类型,需要展示的表格数据
  • loading: Boolean类型,控制表格是否加载状态

Pagination:用于展示分页的组件,参数如下:

  • total: Number类型,总数据量
  • page: Number类型,当前页码
  • pageSize: Number类型,每页展示的数据量
  • onPageChange: Function类型,页码改变时的回调
  • onPageSizeChange: Function类型,pageSize改变时的回调

TableContainer:封装分页控制、加载表格数据等业务逻辑的容器型组件

1、 使用Class封装业务逻辑的组件

首先看下不使用hooks,使用传统的Class组件封装业务逻辑的代码:

import React from 'react';
import TableDisplay from 'components/TableDisplay';
import Pagination from 'components/Pagination';
import { getTableData } from 'apis';    //获取表格数据的api

export default class TableContainer extends React.Component {
    state = {
        data: {
            tableData: [],
            total: 0,
        },
        pagination: {
            page: 1,
            pageSize: 10
        },
        loading: false
    }
    
    componentDidMount() {
        //  首次加载时默认查询第一页数据
        this.loadTable();
    }
    
    handlePageChange: (page) => {
        this.setState({
            pagination: {
                ...this.state.pagination, page
            }
        });
        this.loadTable();
    }
    
    handlePageSizeChange: (pageSize) => {
        this.setState({
            pagination: {
                page: 1,    //pageSize改变时,page自动跳到1
                pageSize
            }
        });
        this.loadTable();
    }
    
    loadTable: () => {
        this.setState({
            loading:true
        });
        
        //  调用APi获取数据
        getTableData(this.state.pagination)
            .then(({data}) => {
                //  数据加载成功
                this.setState({
                    data
                });
            })
            .catch(error => {
                console.log(error);
            })
            .finally(() => {
                this.setState({
                    loading: false
                });
            });
    }
    
    render() {
        const {data, pagination, loading} = this.state;
        
        return (
            <div>
                <TableDisplay data={ data.tableData } loading={ loading } />
                <Pagination
                    onPageChange={ this.handlePageChange }
                    onPageSizeChange={ this.handlePageSizeChange }
                    total={ data.total }
                    { ...pagination } />
            </div>
        );
    }
}

使用Class组件使得业务逻辑代码难以分割和复用;

2、使用带Hooks 的Function组件改写

import React, { useState, useEffect } from 'react';
import TableDisplay from 'components/TableDisplay';
import Pagination from 'components/Pagination';
import { getTableData } from 'apis';    //获取表格数据的api

export default () => {
    //  表格数据
    const [data, setData] = useState({
        tableData: [],
        total: 0
    });
    //  分页状态
    const [pagination, setPagination] = useState({
        page: 1,
        pageSize: 10
    });
    //  加载状态
    const [loading, setLoading] = useState(false);
    
    useEffect(() => {   //分页状态改变时,加载数据
        setLoading(true);
        getTableData(pagination)
            .then(data => {
                setTableData({
                    total: data.total,
                    data: data.data
                });
            })
            .catch(error => {
                console.log(error);
            })
            .finally(() => {
                setLoading(false);
            });
    }, pagination); 
    
    const handlePageChange = page => setPagination({
        page,
        pageSize: pagination.pageSize
    });
    
    const handlePageSizeChange = pageSize => setPagination({
        page: 1,
        pageSize
    });
    
    return (
        <div>
            <TableDisplay data={ data.tableData } loading={ loading } />
            <Pagination
                onPageChange={ handlePageChange }
                onPageSizeChange={ handlePageSizeChange }
                total={ data.total }
                { ...pagination } />
        </div>
    );
}

到这一步,还没达到抽离可复用逻辑的目的

3、使用自定义Hooks分割并抽离业务逻辑

为了实现本文第一行中提到的需求,需要将表格数据加载和分页控制分割开来,使得分页这部分功能可插拔。

于是需要两个自定义Hook

hooks:
    --useTableDataLoader
    --usePagination

useTableDataLoader.js代码如下:

import { useState, useEffect } from 'react';

export default (api, pagination) => {
    const [data, setData] = useState({
        total: 0,
        tableData: []
    });
    const [loading, setLoading] = useState(false);

    useEffect(() => {
        setLoading(true);
        api(pagination)
            .then(data => {
                setData(data);
            })
            .catch(error => {
                console.log(error)
            })
            .finally(() => {
                setLoading(false);
            });
    }, pagination);
    
    return {
        data, loading
    };
}

HookuseTableDataLoader需要两个参数:

  • api:获取表格数据的api
  • pagination:分页状态,可选参数,当api不需要时可不传入,表格只会加载一次

返回一个对象:

  • data: 请求的数据
  • loading:是否处于加载状态

usePagination.js代码如下:

import { useState } from 'react';

export default () => {
    const [pagination, setPagination] = useState({
        page: 1,
        pageSize: 10
    });

    return {
        pagination,
        setPage(page) {
            setPagination({
                page,
                pageSize: pagination.pageSize
            });
        },
        setPageSize(pageSize) {
            setPagination({
                page: 1,
                pageSize
            });
        }
    };
}

HookuseTableDataLoader返回一个对象:

  • pagination:分页信息
  • setPage:修改当前页码的方法
  • setPageSize:修改pageSize的方法

4、使用自定义的Hook实现带分页控制的表格业务组件

TableWithPaginationContainer.jsx代码如下:

import React from 'react';
import TableDisplay from 'components/TableDisplay';
import Pagination from 'components/Pagination';
import { getTableData } from 'apis';    //获取表格数据的api
import useTableDataLoader from 'hooks/useTableDataLoader';
import usePagination from 'hooks/usePagination';

export default () => {
    const { pagination, setPage, setPageSize } = usePagination();
    const { data, loading } = useTableDataLoader(getTableData, pagination);
    const { tableData, total } = data;
    return (
        <div>
            <TableDisplay loading={loading} data={tableData}/>
            <Pagination
                onPageChange={ setPage }
                onPageSizeChange={ setPageSize }
                total={total}
                { ...pagination } />
        </div>
    );
}

5、无分页控制的表格业务组件

TableContainer.jsx代码如下:

import React from 'react';
import TableDisplay from 'components/TableDisplay';
import { getTableData } from 'apis';    //获取表格数据的api
import useTableDataLoader from 'hooks/useTableDataLoader';

export default () => {
    const { data, loading } = useTableDataLoader(getTableData);
    const { tableData, total } = data;
    return (
        <TableDisplay loading={loading} data={tableData}/>
    );
}

这样就实现了可复用逻辑代码的分割和抽离,并使得编写组件的代码更加简介明了!