6

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.

Dan Caragea
  • 1,784
  • 1
  • 19
  • 24

1 Answers1

12

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.

Klaus
  • 1,080
  • 2
  • 10
  • 27
  • Thank you very much for taking the time to look and answer! Great reminder that we need to sometimes take a step back and look from the distance! The code is over engineered because it is extracted from a bigger project, that's why the react-table requirement. In the end, the difference between your code and mine is that yours is manually looping over `values.friends` so it doesn't mind if it mutated or not. Mine relies on react-table which re-renders when it changes. – Dan Caragea Jan 06 '20 at 07:44
  • Hi Dan, I added a possible solution to my answer. could you check it if it also works for you? – Klaus Jan 06 '20 at 09:12
  • Whoaa, it works! I swear I started without that `useEffect` and it didn't work but maybe there was something else at play... I am eager to test this in my project and will get back to you this weekend. – Dan Caragea Jan 07 '20 at 06:14
  • Glad to hear :) – Klaus Jan 07 '20 at 08:33
  • Tested in the project, it works! Thanks for saving my sanity, I owe you one :) – Dan Caragea Jan 11 '20 at 11:03
  • thank you, ---enableReinitialize--- saved me some time! – faramarz razmi Jul 05 '21 at 10:23
  • thank you, ---enableReinitialize--- saved me some time! – faramarz razmi Jul 05 '21 at 10:24