ps:刚开始用react,所以我写的很烂,并且也不是很理解为什么我按照别人的教程一一做了就是失败了。
一、代码结构
1. 父组件A结构中有B、C两个子组件
- useEffect中请求categoryList传递给B,请求list传递给C的table使用
- tabActive状态传递给C的tab使用
- onFilter方法用来筛选table中数据,由B组件表单提交时触发
2. B组件中有表单,接收父组件传递过来的categoryList和onFilter
3. C组件:一个Tab组件和一个Table组件
- 表格每一行有操作onBatch,点击会触发A组件中的批量更新数据方法
二、问题描述
当在C组件切换tab或者onBatch时,只要A中有state改变,都会触发B组件的重复渲染,实际B组件的依赖数据是没有更新的,比较浪费
三、解决过程
1. 使用React.memo将B组件包裹,使用useCallback包裹onFilter方法。结果:依旧会引起B组件重复渲染。
2. 在第一步的结果上使用useMemo又缓存了categoryList。结果:依旧重复渲染。
3. 查看官方文档找到为什么1和2失败。
4. 寻求大佬朋友的帮助,告知我代码中很多useCallback都是无效的,让我使用useCallback包裹B组件,并传递依赖项categoryList和onFilter。结果:依旧重复渲染,在方法中的打印语句多次执行
5. 抱着试试的心态,用useMemo包裹了B组件并传递依赖项。结果:解决!
四、总结:需要完整阅读一遍官方文档,以及hooks的使用规范,再接再厉!
五、源码(删除部分无关代码)
父组件A
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
DataType,
FilterProductInfo,
PageInfo,
Category,
TableComponentRef
} from "../type";
import Filter from "./components/Filter";
import Table from "./components/Table";
import GoodsApi from "@/api/interface/goods";
import { TabsProps, message } from "antd";
import { productStatusConst } from "../const";
export default function GoodsManagement() {
const [categoryList, setCatagoryList] = useState<Category[]>([]);
const [filterData, setFilterData] = useState<FilterProductInfo>({});
const [tabActive, setTabActive] = useState<number>(0);
const [tabItems, setTabItems] = useState<TabsProps["items"]>(
productStatusConst.list
);
const [tableData, setTableData] = useState<DataType[]>([]);
const [page, setPage] = useState<PageInfo>({ pageIndex: 1, pageSize: 10 });
const [pageTotal, setPageTotal] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const tableRef = useRef<TableComponentRef>(null);
const onFilter = useCallback(
(form: FilterProductInfo) => {
const data = {
...form,
status: form.status ?? tabActive
};
setFilterData(data);
setTabActive(data.status);
setPage({ pageIndex: 1, pageSize: 10 });
},
[tabActive]
);
// 表格批量操作
const onOperate = async ({
type,
productIds
}: {
type: number;
productIds: string[];
}) => {
console.log("执行批量操作", type, productIds);
const data = { productIds };
try {
if (type === productStatusConst.USING) {
await GoodsApi.unableProduct(data);
} else {
await GoodsApi.enableProduct(data);
}
} catch (error) {
console.log(error);
}
message.success("操作成功");
// 批量操作完成后列表恢复到第一页 清空选中项
setPage({ ...page, pageIndex: 1 });
getProductCount({});
tableRef.current?.setSelectedKeys([]);
};
const getProductList = useCallback(
async (data: FilterProductInfo) => {
console.log("getProductList调用了,当前数据", data);
setLoading(true);
try {
const { list, totalSize } = await GoodsApi.getProductList(data);
setTableData(list);
setPageTotal(totalSize);
} catch (error) {
console.log("error==>", error);
} finally {
setLoading(false);
}
},
[setTableData, setPageTotal, setLoading]
);
const getCategoryList = useCallback(async () => {
console.log("调用getCategoryList了");
const { list } = await GoodsApi.getCategoryList();
setCatagoryList(list);
}, [setCatagoryList]);
const getProductCount = useCallback(
async (data: FilterProductInfo) => {
try {
const { enableCount, incompleteCount, unableCount } =
await GoodsApi.getProductCount(data);
const tabsItem = productStatusConst.list.map(item => {
const labelMap = {
[productStatusConst.USING]: `${item.label} ${enableCount}`,
[productStatusConst.STOP]: `${item.label} ${unableCount}`,
[productStatusConst.TO_BE_COMPLETE]: `${item.label} ${incompleteCount}`
};
return { ...item, label: labelMap[item.value] || "" };
});
setTabItems(tabsItem);
} catch (error) {
console.log("error==>", error);
}
},
[setTabItems]
);
useEffect(() => {
console.log("当前tab选中", tabActive);
getProductList({ ...page, status: tabActive, ...filterData });
}, [page, tabActive, filterData, getProductList]);
useEffect(() => {
getCategoryList();
}, [getCategoryList]);
useEffect(() => {
getProductCount({});
}, [getProductCount]);
return (
<>
{useMemo(
() => (
<Filter categoryList={categoryList} onFilter={onFilter} />
),
[categoryList, onFilter]
)}
<Table
ref={tableRef}
tableData={tableData}
page={page}
total={pageTotal}
tabActive={tabActive}
loading={loading}
tabItems={tabItems}
onChange={(page: PageInfo) => setPage(page)}
onChangeTab={(active: string) => setTabActive(Number(active))}
onBatch={payload => onOperate(payload)}
/>
</>
);
}
子组件B
import { Button, Form, Input, Row, Col, Select, Card } from "antd";
import { Category, FilterProductInfo } from "../../type";
import { productStatusConst, platformConst } from "../../const";
const SearchForm = ({
categoryList,
onFilter
}: {
categoryList: Category[];
onFilter: (data: FilterProductInfo) => void;
}) => {
console.log("筛选组件渲染了!");
const [form] = Form.useForm();
const onSubmit = () => {
const data = form.getFieldsValue();
onFilter(data);
};
const onCancel = () => {
form.resetFields();
};
return (
<Card bordered={false} className="mt-4">
<Form layout={"inline"} form={form}>
<Row gutter={[16, 16]} wrap>
<Col span={8}>
<Form.Item label="商品名称" name="productName">
<Input placeholder="关键字" allowClear />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="商品品类" name="categoryId">
<Select placeholder="请选择商品类目" allowClear>
{categoryList.map(item => (
<Select.Option key={item.categoryId} value={item.categoryId}>
{item.categoryDisplayName}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="商品品牌" name="brandName">
<Input placeholder="关键字" allowClear />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label="商品状态"
name="status"
initialValue={productStatusConst.USING}
>
<Select placeholder="请选择商品状态" allowClear>
{productStatusConst.list.map(item => (
<Select.Option key={item.value} value={item.value}>
{item.label}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="商品平台" name="platform">
<Select placeholder="请选择销售平台(可多选)" allowClear>
{platformConst.map(item => (
<Select.Option key={item.value} value={item.value}>
{item.text}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}></Col>
<Col span={8}>
<Button onClick={onSubmit}>查询</Button>
<Button className="ml-2" onClick={onCancel}>
重置
</Button>
</Col>
</Row>
</Form>
</Card>
);
};
export default SearchForm;
子组件C
import { Table, Button, Tabs, Card, Tooltip, message } from "antd";
import { ExclamationCircleFilled } from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import { DataType, GoodsTableProps } from "../../type";
import MainImage from "./MainImage";
import {
deliveryTimeConst,
productStatusConst
} from "../../const";
import { useState, useMemo, forwardRef, useImperativeHandle } from "react";
import PopConfirm from "@/components/PopConfirm";
const GoodsTable = forwardRef(function GoodsTable(
{
tableData,
page,
total,
tabActive,
loading,
tabItems,
onChange,
onChangeTab,
onBatch
}: GoodsTableProps,
ref
) {
const columns: ColumnsType<DataType> = [
{
title: "商品名称",
dataIndex: "productName",
fixed: "left",
width: 120,
},
{
title: "零售价",
dataIndex: "priceTag",
fixed: "left",
width: 80,
render: text => {
return `¥${Math.round(text / 100).toFixed(2)}`;
}
},
{
title: "品牌名称",
dataIndex: "brandName",
width: 80,
render: text => (
<Tooltip placement="topLeft" title={text}>
{text}
</Tooltip>
)
},
{
title: "商品品类",
dataIndex: "categoryName",
width: 80
},
{
title: "商品库存",
key: "inventory",
dataIndex: "inventory",
width: 80
},
{
title: "运费险",
dataIndex: "isSupportFreightInsurance",
width: 90,
render: text => {
return text === 1 ? "支持" : "不支持";
}
},
{
title: "发货时间",
dataIndex: "deliveryTime",
width: 120,
render: text => {
return deliveryTimeConst[text];
}
},
{
title: "发货快递",
key: "express",
dataIndex: "express",
width: 80
},
{
title: "商品状态",
key: "status",
dataIndex: "status",
width: 80,
render: text => {
return productStatusConst[text];
}
},
{
title: "操作",
key: "operation",
dataIndex: "operation",
fixed: "right",
align: "center",
width: 100,
render: (_text, record) => {
return (
<div>
{record.status === productStatusConst.TO_BE_COMPLETE ? (
""
) : (
<PopConfirm
title="提示"
description={`确定要${operateText}该商品吗?`}
onConfirm={() =>
onBatch({
type: tabActive,
productIds: [String(record.productId)]
})
}
>
{record.status !== (undefined || null) ? (
<Button type="link" className="text-theme">
{record.status === 1 ? "启用" : "禁用"}
</Button>
) : (
""
)}
</PopConfirm>
)}
</div>
);
}
}
];
const onPageChange = (page: number, pageSize: number) => {
// 将分页信息传递给父组件
onChange({
pageIndex: page,
pageSize: pageSize
});
};
const operateText = useMemo(() => {
if (tabActive === productStatusConst.USING)
return productStatusConst[productStatusConst.STOP];
if (tabActive === productStatusConst.STOP)
return productStatusConst[productStatusConst.USING];
}, [tabActive]);
const [batchLoading, setBatchLoading] = useState<boolean>(false);
const onBatchOperation = async () => {
if (!selectedRowKeys?.length) return message.warning("请选择商品");
setBatchLoading(true);
await onBatch({ type: tabActive, productIds: selectedRowKeys });
setBatchLoading(false);
};
const paginationConfig = {
current: page.pageIndex,
pageSize: page.pageSize,
total: total,
pageSizeOptions: [10, 20, 50],
onChange: onPageChange
};
useImperativeHandle(
ref,
() => {
return {
setSelectedKeys
};
},
[]
);
return (
<Card bordered={false} className="mt-4">
<Tabs
items={tabItems}
activeKey={String(tabActive)}
onChange={active => onChangeTab(active)}
/>
<Table
rowKey="productId"
loading={loading}
columns={columns}
dataSource={tableData}
scroll={{ x: 1200 }}
pageSizeOptions={[10, 20, 50]}
pagination={paginationConfig}
/>
</Card>
);
});
export default GoodsTable;