Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have this scenario where I load up a form's data from a server (let's say a user entity with the user's list of friends).

The form has the list of friends with editable names rendered as a table with react-table 7. The problem I am facing is that whenever I try to edit the name of a friend in this list, I can only type a single character and then the input loses focus. I click the input again, type 1 char, it loses focus again.

I created a codesandbox to illustrate the problem: https://codesandbox.io/s/formik-react-table-hr1l4

I understand why this happens - the table re-renders every time I type because the formik state changes - but I am unsure how to prevent this from happening. I useMemo-ed and useCallback-ed all I could think of (also React.memo-ed the components in the hope it would prevent the problem), yet no luck so far.

It does work if I remove the useEffect in Friends, however, that will make the table to not update after the timeout expires (so it won't show the 2 friends after 1s). Any help is greatly appreciated...I've been stuck on this problem for the whole day.

like image 454
Dan Caragea Avatar asked Jan 04 '20 23:01

Dan Caragea


People also ask

How do you make a row editable in React?

In your column array that you pass into react table you need to create a button who's onClick function takes a callback to edit your data to add an isEditing: true so you will handle turning the row to edit mode from outside of the table.

What is FieldArray in Formik?

<FieldArray /> is a component that helps with common array/list manipulations. You pass it a name property with the path to the key within values that holds the relevant array. <FieldArray /> will then give you access to array helper methods via render props.

How do I update my Formik data?

You can enable enableReinitialize property of Formik to update the field.


1 Answers

Wow you really had fun using all the different hooks React comes with ;-) I looked at your codesandbox for like 15 minutes now. My opinion is that it is way over engineered for such a simple task. No offence. What I would do:

  • Try to go one step back and start simple by refactoring your index.js and use the FieldArray as intended on the Formik homepage (one render for every friend).
  • As a next step you can build a simple table around it
  • Then you could try to make the different fields editable with input fields
  • If you really need it you could add the react-table library but I think it should be easy to implement it without it

Here is some code to show you what I mean:

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);

Everything is one component now. You can now refactor it into different components if you like or check what kind of hooks you can apply ;-) Start simple and make it work. Then you can continue with the rest.

Update:

When you update the Friends component like this:

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);

It seems to not loose focus anymore. Could you also check it? I removed the useEffect block and the table works directly with the formikSlice. I guess the problem was that when you changed an input that the Formik values were updated and the useEffect block was triggered to update the internal state of the Friends component causing the table to rerender.

like image 54
Klaus Avatar answered Oct 17 '22 07:10

Klaus