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.