Location>code7788 >text

Implementing Editable Table in a React Project

Popularity:111 ℃/2024-08-08 16:18:31

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:

  1. Tables saved in real time, i.e. all cells can be edited directly.
  2. Editable row forms, i.e. you need to manually click Edit to get to the row editing state.

Contrast the two forms of interaction:

  1. 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.
  2. 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 thatcompilerYou 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:

  1. It is not possible to check each line individually.
  2. Components are fully controlled and input can lag badly when there are a lot of forms.

Pros:

  1. Very flexible.
  2. Don't think about it.Form of the dependency rendering problem.
  3. 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, thenamePath If it is an array of strings["user", "name"]If it is not, it will be collected as an object structure IfnamePath Contains integers, which are collected as arrays["users", 0, "name"]users[0].name
in which will be exposed the maintenance of thefields Metadata and Add/Delete/Move operations of theopeartion , then withtable Combined, the realization becomes much simpler.
included among thesefield The object containskey together withnamekey 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 togetherparentNamePrefix 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 addtrue The parameters also get all the data.
Inside itself is also a However, the addition ofisList 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 onValuesChangestore 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
iShot_2024-06-22_16.26.
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:

  1. FormItem When adding or deleting a position change
  2. ought toCell 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 inshouldCellUpdate 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
iShot_2024-06-22_17.00.
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