如何将自定义数据表拆分为模块化组件



我正在寻找一些指导,以了解在开发自定义数据表时什么是最佳实践。

背景:我的任务是创建一个表组件,当用户分页、排序和/或搜索时,它同时具有静态和异步加载功能。目前,我有一个单独的表来处理这两个分支,但它变得有点臃肿,我想把这两个分行分开。

到目前为止,我有两个想法:

  1. 有一个表组件,它将显示数据并为开发人员打开特定功能(排序、搜索、分页(。然后有两个更高阶的组件来包装原始的表组件,当用户对表进行排序、搜索或分页时,该组件将处理异步或静态加载
  • 我看到的这个实现的问题是,这些高阶组件将与原始的Table组件实现非常耦合,而其中一个(下面显示的是AsyncSubscription(采用了原始Table组件不准备在Table组件中使用的api数据的道具
  1. 拥有一个表组件,该组件将显示数据并为开发人员打开特定功能(排序、搜索、分页(。为原始表组件创建一个包装组件,该组件将处理重新获取数据(异步数据加载将排序、搜索和分页参数传递到后端(,并创建另一个包装部件,该组件包装将转换原始数据集的原始表组件(静态数据加载在前端执行排序、搜索、分页(
  • 我在这个实现中看到的问题是,必须通过这个包装组件将大量道具中继到原始的Table组件中

什么解决方案在理论上听起来更好,更符合React基于类的组件最佳实践?我已经试过了我的第一个想法,它看起来像这样:

export class Table extends React.Component {
static propTypes = {
headings: PropTypes.object,
data: PropTypes.arrayOf(PropTypes.object),
sortable: PropTypes.arrayOf(PropTypes.string),
searchable: PropTypes.arrayOf(PropTypes.string),
pageSize: PropTypes.number,
totalRecords: PropTypes.number,
loadData: PropTypes.func,
loading: PropTypes.bool,
}

load() {
// call external loadData prop if it exists
// or
// perform synchronous sort, search, pagination 
}

renderSortableColumns() {
// render sort column headers if sortable prop exists
}

renderSearch() {
// render search if searchable prop exists
}

renderPagination() {
// render table pagiantion if pageSize prop exists
} 


render() {
// render table
}
}

export function withStaticSubscription(Table) {
return class WithStaticSubscription extends React.Component {
static propTypes = {
data: PropTypes.any
}
constructor(props) {
super(props);
this.load = this.load.bind(this);
this.state = { displayData: [], totalRecords: 0 };
}
load(sort, search, currentPage, pageSize) {
// perform sort, search, and pagination with original static data set

// set data and pass it into original Table component
this.setState({ displayData, totalRecords });
}
render = () => {
const { displayData, totalRecords } = this.state;

return <Table
{...this.props}
data={displayData}
loadData={this.load}
totalRecords={totalRecords}
/>;
}
};
}
export function withAsyncSubscription(Table) {
return class WithAsyncSubscription extends React.Component {
static propTypes = {
loadData: PropTypes.func, // api to get new table data
transformData: PropTypes.func // custom transformer that prepares data for table
}
static defaultProps = {
loadData: () => {},
transformData: () => {}
}
constructor(props) {
super(props);
this.load = this.load.bind(this);
this.state = { displayData: [], totalRecords: 0 };
}
load = async(sort, search, currentPage, pageSize) => {
const { loadData, transformData } = this.props;
const searchParams = {
search: false,
page: currentPage + 1,
rows: pageSize,
sortColumn: sort.column,
sortOrder: sort.order
};
this.setState((prev) => { return { ...prev, loading: true }; });
const { data, totalRecords } = await loadData(searchParams);
const displayData = transformData(data);
this.setState({ totalRecords: totalRecords, displayData, loading: false });
}
render = () => {
const { loading, displayData, totalRecords } = this.state;
return <Table
{...this.props}
data={displayData}
loadData={this.load}
loading={loading}
totalRecords={totalRecords}
/>;
}
};
}


export const TableWithAsyncSubscription = withAsyncSubscription(Table);
export const TableWithStaticSubscription = withStaticSubscription(Table);

我看到的这个实现的问题是,这些高阶组件将与原始表组件实现完全耦合,而其中一个(下面显示的是AsyncSubscription(采用了原始表组件不准备在表组件中使用的api数据的道具。

这通常不是一件坏事,事实恰恰相反。你所描述的基本上是组合,这是解决这一问题的正确工具,以及从架构的角度解决模块化的React方法。从本质上讲,你最初压倒一切的本能是非常正确的!

这取决于您是否打算将withAsyncSubscription与非表的其他组件一起使用。如果你不这样做,你就不会从解耦中获得太多好处,因为你只是把粘合代码移到了消费者身上(见下文(。然而,如果你想以消费者特有的方式进一步修改这些道具,我下面描述的方式可能会很有用。它让一切都可见,并提供了逃生通道,而不需要假设事物是如何绑定的。

如果你真的希望它进一步解耦,我根本不会使用HOC,而是使用渲染道具模式。

return class WithAsyncSubscription extends React.Component {
static propTypes = {
loadData: PropTypes.func, // api to get new table data
transformData: PropTypes.func // custom transformer that prepares data for table
children: PropTypes.func
}
static defaultProps = {
loadData: () => {},
transformData: () => {}
children: () => null
}
constructor(props) {
super(props);
this.load = this.load.bind(this);
this.state = { displayData: [], totalRecords: 0 };
}
load = async(sort, search, currentPage, pageSize) => {
const { loadData, transformData } = this.props;
const searchParams = {
search: false,
page: currentPage + 1,
rows: pageSize,
sortColumn: sort.column,
sortOrder: sort.order
};
this.setState((prev) => { return { ...prev, loading: true }; });
const { data, totalRecords } = await loadData(searchParams);
const displayData = transformData(data);
this.setState({ totalRecords: totalRecords, displayData, loading: false });
}
render = () => {
const { loading, displayData, totalRecords } = this.state;
return this.props.children({
data: displayData
loadData: this.load
loading: loading
totalRecords: totalRecords
})
}
};

现在在您的用例中,您可以执行以下操作:

<WithAsyncSubscription>
{(asyncProps) => <Table {...asyncProps} /> {/* You can add any other props from the containing component here */}
<WithAsyncSubscription>

这样做的好处是,如果你需要结合多种功能,你可以这样做:

<WithAnotherFeature>
{(featureProps) =>
<WithAsyncSubscription>
{(asyncProps) => <Table {...asyncProps} {...featureProps} /> {/* You can add any other props from the containing component here */}
<WithAsyncSubscription>
}
</WithAnotherFeature>

在这里,我将它们连续分布,但由于没有假定的顺序,这比HOC要好,在HOC中,你可以意外地将它们相互耦合,它们开始变得依赖于顺序。相反,合并逻辑留给消费者进行绑定。

值得注意的是,React钩子使其比渲染道具模式好得多,但除非您将旧样式的类组件转换为";新的";功能组件。Hooks基本上已经在很大程度上扼杀了作为抽象概念的HOC。这并不是说HOC很糟糕——只是不太常见。钩子/渲染道具要近得多,基本上都在做同样的事情。一般来说,这些对消费者来说可能会好一点,因为它们是实现状态逻辑组合的机制,不会像普通的HOC那样做出太多假设(比如你要渲染什么以及道具如何映射(。

不过,将组件转换为使用钩子是否值得花费时间/精力,取决于您。如果渲染道具对你有用,我不会。

最新更新