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.
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.
<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.
You can enable enableReinitialize property of Formik to update the field.
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:
FieldArray
as intended on the Formik homepage (one render for every friend).react-table
library but I think it should be easy to implement it without itHere 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With