We are.Kangaroo Cloud Stack UED Team, is committed to building an excellent one-stop data middleware product. We always maintain the spirit of craftsmanship and explore the front-end path to accumulate and spread the value of experience for the community.
This article was written by Jia Lan
Editable forms are a relatively common way of interacting with form data in number stack products, and generally support dynamic addition, deletion, sorting and other basic functions.
Interaction Classification
Editable forms are generally in two interactive forms:
- Tables saved in real time, i.e. all cells can be edited directly.
- Editable row forms, i.e. you need to manually click Edit to get to the row editing state.
Contrast the two forms of interaction:
- The first interaction is more friendly, but the corresponding performance overhead can be very high, without the need to manually enter the cell editing state.
- For the second type of interaction, more scenarios are in a large amount of data, do not need to be modified frequently, or batch updates will have a large performance impact on the back-end database operations will have a large impact on the scenario. It also has a nice benefit in that
compiler
You can roll back the filled data when the status is "Filled".
The vast majority of counting stack products utilize the first type of interaction.
To realize an editable table, Table component is indispensable, whether to introduce Form for data collection, but also to analyze the specific scenarios.
If Form is not introduced, and you manage the data collection yourself, the general implementation is as follows.
const EditableTable = () => {
const [dataSource, setDataSource] = useState([]);
const handleAdd = () => {
const newData = {
key: shortid(),
name: 'New User',
};
setDataSource([...dataSource, newData]);
};
const handleDelete = (key) => {
const newData = (item => !== key);
setDataSource(newData);
};
const handleChange = (value, key, field) => {
const newData = (item => {
if ( === key) {
return { ...item, [field]: value };
}
return item;
});
setDataSource(newData);
};
const handleMove = (key, direction) => {
const index = (item => === key);
const newData = [...dataSource];
const [item] = (index, 1);
(direction === 'up' ? index - 1 : index + 1, 0, item);
setDataSource(newData);
};
const columns = [
{
title: 'Name',
dataIndex: 'name',
render: (text, record) => (
<Input
value={text}
onChange={e => handleChange(, , 'name')}
/>
),
},
{
title: 'Action',
dataIndex: 'action',
render: (_, record) => (
<span>
<Button
onClick={() => handleMove(, 'up')}
>
upward shift
</Button>
<Button
onClick={() => handleMove(, 'down')}
>
downward shift
</Button>
<Button onClick={() => handleDelete()}>
removing
</Button>
</span>
),
},
];
return (
<div>
<Button
onClick={handleAdd}
>
increase
</Button>
<Table
columns={columns}
dataSource={dataSource}
pagination={false}
/>
</div>
);
};
export default EditableTable;
Problems:
- It is not possible to check each line individually.
- Components are fully controlled and input can lag badly when there are a lot of forms.
Pros:
- Very flexible.
- Don't think about it.
Form
of the dependency rendering problem. - Front-end paging of forms is possible, which can somewhat solve performance issues.
If you use theForm
, the most correct way to do this is through to realize. Form When binding fields, the
namePath
If it is an array of strings["user", "name"]
If it is not, it will be collected as an object structure If
namePath
Contains integers, which are collected as arrays["users", 0, "name"]
⇒ users[0].name
。 in which will be exposed the maintenance of the
fields
Metadata and Add/Delete/Move operations of theopeartion
, then withtable
Combined, the realization becomes much simpler.
included among thesefield
The object containskey
together withname
,key
is monotonically increasing without duplicates, and if that data is deleted, thename
is its subscript in the array.
We're here forFormItem
registeredname
Although it is[0, "name"]
But it's in the hit the nail on the head
The components are automatically put together
parentNamePrefix
prefix, which means it will eventually become[”users”, 0, “name”]
。
<Form form={form}>
< name="users">
{(fields, operation) => (
<>
<Table
key="key"
dataSource={fields}
columns={[
{
title: "name and surname",
key: "name",
render: (_, field) => (
<FormItem name={[, "name"]}>
<Input />
</FormItem>
),
},
{
title: "manipulate",
key: "actions",
render: (_, field) => (
<Button
onClick={() =>
()
}
>
removing
</Button>
),
},
]}
pagination={{ pageSize: 3 }}
/>
<Button onClick={() => ({ name: "Jack" })}>
increase
</Button>
</>
)}
</>
</Form>
As we can see, using the implementation, we can even use paging, as we have done through the()
Check that the data is normal.
Why was the first page of form data destroyed saved?
defaultpreserve
because oftrue
fields can still hold data when destroyed, they just need to be destroyed via thegetFieldsValue(true)
to get it, but for , no need to add
true
The parameters also get all the data. Inside itself is also a
However, the addition of
isList
To differentiate, not only the sub-items in the List, but also itself will be registered. As shown in the following figure, there are 5 data items in the form, due to paging only the current page of the data form will be registered in the Form to collect.
The extra will putusers
It is also collected as a separate field.
Then, in thegetFieldsValue
In the source code, it takes the registered value directly.
Therefore, the use of Completing the paging is possible when analyzed at the source code level, but I haven't actually seen many people use it in conjunction with this.
appliance
Case 1
As an example, the run parameter is implemented using theTable
customizablecomponents
, inEditableCell
in which you then define how the form is rendered.
const RunParamsEditTable = () => {
const [dataSource, setDataSource] = useState([])
const components = {
body: {
row: EditableFormRow,
cell: EditableCell,
},
};
const initColumns = () => {
return [
// xxxfield
];
};
const columns = initColumns().map((col) => {
if (!) {
return col;
}
return {
...col,
onCell: (record, index) => ({
index,
record,
editable: ,
dataIndex: ,
title: record[] || ,
errorTitle: ,
save,
// There's a lot of other state that needs to be passed
}),
};
});
return (
<div>
<Table components={components} dataSource={dataSource} columns={columns} />
<span onClick={}>Adding Run Parameters</span>
</div>
);
};
existEditableCell
In this way, it is usually necessary to pass a large number of props to communicate with the parent component, and the table column definition and the form definition are split into two components, which I feel is too fragmented, and for the vast majority of products in theEditableTable
For example, using a customizedcomponents
Kind of a big deal.
const EditableCell = ({ editable, dataIndex, children, save, ...restProps }) => {
const renderCell = () => {
switch (dataIndex) {
case 'name':
return (
< name={dataIndex} onChange={(v) => save(v)}>
<Input />
</>
);
// All other fields
}
};
return <td>{editable ? renderCell() : children}</td>;
};
In the code, the actual customization againRow
to create aForm
This is the only way to edit multiple lines at the same time, and Form is only used for checksums, which are passed later.save
to be collected manually. If instead of the above form, then this will become well-maintained by synchronizing the list data to the upper level in the onValuesChange
store
Center.
personal viewTable
customizablecomponents
Should be in the form of rows or cells to maintain some of their own state should be considered, such as rows and columns drag and drop, cells can be edited in the state of the switch and other scenarios to use.
Case 2
Each form item is a drop-down box and the drop-down options are requested through a cascade.
Here, we might do this by maintaining a state to hold a list of tables that don't correspond to the database, and using thedbId
for the key.
const [tableOptionsMap, setTableOptionsMap] = useState(new Map())
existcolumns render
The corresponding tableOptions are consumed directly in the
<FormItem dependencies={[["list", , "dbId"]]}>
{() => {
const dbId = (["list", , "dbId"]);
const tableOptions = (dbId);
return (
<FormItem name={[, "table"]}>
<Select options={tableOptions} />
</FormItem>
);
}}
</FormItem>;
This was all fine, but by the time I added data to the order of a hundred lines, the lag was already very noticeable
Since we are puttingstate
stored in the parent component, each request will cause thetable
If you add states like loading, the number of renderings will be even higher.Table
components are not optimized for rerender behavior by default, the parent component is updated if thecolumns
provides a custom render method that corresponds to each of theCell
will be re-rendered.
For this situation we need to optimize, according to theshouldCellUpdate
Customize the rendering timing.
Then the rendering timing for each Cell should be:
-
FormItem
When adding or deleting a position change - ought to
Cell
Consumption counterparttableOptions
change
The first case is easy to determine. center
Referring to subscripts, just compare
shouldCellUpdate: (prev, curr) => {
return !== ;
}
In the second case, we don't know directly.tableOptions
If there is a change, so you need to write your own hooksusePreviousStateRef
, here's a very important point to note: the return of theref
rather than in
shouldCellUpdate
There will be closure issues with using it in
const usePreviousStateRef = <T>(state: T): <T> => {
const ref = <typeof state>();
useEffect(() => {
= state;
}, [state]);
return ref;
};
const prevTableOptionsMapRef = usePreviousStateRef(tableOptionsMap);
Combined, then, the re-rendering conditions become
shouldCellUpdate: (prev, curr) => {
// Render directly on position changes
if ( ! == ) return true;
// Re-render only the rows of the data table where the dropdown has changed
const dbId = (['list', , 'dbName']), ?
const prevTableInfo = ? .get(dbId); const currTableInfo
const currTableInfo = tableOptionsMap?.get(dbId);
return prevTableInfo ! == currTableInfo; }, const currTableInfo = tableOptionsMap?
},
The breakdown is much smoother after the change
pass (a bill or inspection etc)shouldCellUpdate
This solves the performance problem, but if the render relies on an external state, you'll have to save the prevState yourself to determine it.
Summary:
The combination of + Table meets most of the requirements, so this is the first thing you should consider in subsequent development, and then try to use custom components when there is a respective state in each row that needs to be maintained, and never mix state and Form!
Adequate performance considerations also need to be taken into account, especially when faced with the presence of a large number of drop-down boxes.
ultimate
Welcome to [Kangaroo Cloud Digital Stack UED team]~
Kangaroo Cloud Digital Stack UED team continues to share the results of technology for the majority of developers, successively participated in the open source welcome star
- Big Data Distributed Task Scheduler - Taier
- Lightweight Web IDE UI framework - Molecule
- SQL Parser Project for Big Data - dt-sql-parser
- Kangaroo Cloud Digital Stack front-end team code review engineering practices document - code-review-practices
- A faster, more flexible configuration and easier to use module packager - ko
- A component testing library for antd - ant-design-testing