如何使用 formik 2 和 react-table 7 渲染 editable table?

How to render an editable table with formik 2 and react-table 7?

我有这样一个场景,我从服务器加载表单的数据(假设是一个用户实体和用户的朋友列表)。

该表单包含带有 editable 名字的朋友列表,呈现为带有 react-table 7 的 table。 我面临的问题是,每当我尝试编辑此列表中朋友的姓名时,我只能键入一个字符,然后输入失去焦点。我再次单击输入,键入 1 个字符,它再次失去焦点。

我创建了一个codesandbox来说明这个问题:https://codesandbox.io/s/formik-react-table-hr1l4

我理解为什么会发生这种情况 - table 每次我键入时都会重新呈现,因为 formik 状态发生变化 - 但我不确定如何防止这种情况发生。我 useMemo-ed 和 useCallback-ed 所有我能想到的(也 React.memo-ed 组件希望它能防止问题),但到目前为止没有运气。

如果我删除 Friends 中的 useEffect 它确实有效,但是,这将使 table 在超时到期后不更新(因此它不会显示1 秒后有 2 个朋友)。 非常感谢任何帮助...我整天都被这个问题困住了。

哇,你真的很喜欢使用 React 附带的所有不同的钩子 ;-) 我现在看了你的 codesandbox 大约 15 分钟。我的观点是,对于这样一个简单的任务来说,它的设计有点过头了。没有恶意。我会做什么:

  • 尝试退后一步,通过重构您的 index.js 并按照 Formik 主页上的预期使用 FieldArray(每个朋友一个渲染)。
  • 作为下一步,您可以围绕它构建一个简单的 table
  • 那么你可以尝试用输入字段
  • 制作不同的字段editable
  • 如果你真的需要它,你可以添加 react-table 库,但我认为没有它应该很容易实现它

这里有一些代码可以向您展示我的意思:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import { Formik, Form, FieldArray, Field } from "formik";
import Input from "./Input";
import "./styles.css";

const initialFormData = undefined;

function App() {
  const [formData, setFormData] = useState(initialFormData);

  useEffect(() => {
    // this is replacement for a network call that would load the data from a server
    setTimeout(() => {
      setFormData({
        id: 1,
        firstName: "First Name 1",
        friends: [
          { id: 2, firstName: "First Name 2", lastName: "Last Name 2" },
          { id: 3, firstName: "First Name 3", lastName: "Last Name 3" }
        ]
      });
    }, 1000);
    // Missing dependency array here
  }, []);

  return (
    <div className="app">
      {formData && (
        <Formik initialValues={formData} enableReinitialize>
          {({ values }) => (
            <Form>
              <Input name="name" label="Name: " />
              <FieldArray name="friends">
                {arrayHelpers => (
                  <div>
                    <button
                      onClick={() =>
                        arrayHelpers.push({
                          id: Math.floor(Math.random() * 100) / 10,
                          firstName: "",
                          lastName: ""
                        })
                      }
                    >
                      add
                    </button>
                    <table>
                      <thead>
                        <tr>
                          <th>ID</th>
                          <th>FirstName</th>
                          <th>LastName</th>
                          <th />
                        </tr>
                      </thead>
                      <tbody>
                        {values.friends && values.friends.length > 0 ? (
                          values.friends.map((friend, index) => (
                            <tr key={index}>
                              <td>{friend.id}</td>
                              <td>
                                <Input name={`friends[${index}].firstName`} />
                              </td>
                              <td>
                                <Input name={`friends[${index}].lastName`} />
                              </td>
                              <td>
                                <button
                                  onClick={() => arrayHelpers.remove(index)}
                                >
                                  remove
                                </button>
                              </td>
                            </tr>
                          ))
                        ) : (
                          <tr>
                            <td>no friends :(</td>
                          </tr>
                        )}
                      </tbody>
                    </table>
                  </div>
                )}
              </FieldArray>
            </Form>
          )}
        </Formik>
      )}
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

现在一切都是一个组件。如果您愿意,您现在可以将它重构为不同的组件,或者检查您可以应用哪种挂钩 ;-) 从简单开始,让它发挥作用。然后你就可以继续剩下的了。

更新:

当您像这样更新好友组件时:

import React, { useCallback, useMemo } from "react";
import { useFormikContext, getIn } from "formik";
import Table from "./Table";
import Input from "./Input";

const EMPTY_ARR = [];

function Friends({ name, handleAdd, handleRemove }) {
  const { values } = useFormikContext();

  // from all the form values we only need the "friends" part.
  // we use getIn and not values[name] for the case when name is a path like `social.facebook`
  const formikSlice = getIn(values, name) || EMPTY_ARR;

  const onAdd = useCallback(() => {
    const item = {
      id: Math.floor(Math.random() * 100) / 10,
      firstName: "",
      lastName: ""
    };
    handleAdd(item);
  }, [handleAdd]);

  const onRemove = useCallback(
    index => {
      handleRemove(index);
    },
    [handleRemove]
  );

  const columns = useMemo(
    () => [
      {
        Header: "Id",
        accessor: "id"
      },
      {
        Header: "First Name",
        id: "firstName",
        Cell: ({ row: { index } }) => (
          <Input name={`${name}[${index}].firstName`} />
        )
      },
      {
        Header: "Last Name",
        id: "lastName",
        Cell: ({ row: { index } }) => (
          <Input name={`${name}[${index}].lastName`} />
        )
      },
      {
        Header: "Actions",
        id: "actions",
        Cell: ({ row: { index } }) => (
          <button type="button" onClick={() => onRemove(index)}>
            delete
          </button>
        )
      }
    ],
    [name, onRemove]
  );

  return (
    <div className="field">
      <div>
        Friends:{" "}
        <button type="button" onClick={onAdd}>
          add
        </button>
      </div>
      <Table data={formikSlice} columns={columns} rowKey="id" />
    </div>
  );
}

export default React.memo(Friends);

好像没有散焦了。你也可以检查一下吗?我删除了 useEffect 块,table 直接与 formikSlice 一起工作。我想问题在于,当您更改输入时,Formik 值已更新并且 useEffect 块被触发以更新 Friends 组件的内部状态,导致 table 重新呈现。