Managing sign-in / sign-up form is not that difficult, everyone does it, but what if you've an editable list to manage which has hundreds of items, e.g. a todo app? Worry not formik is to the rescue.
Why Formik
- Getting values in and out of form state is very easy and straightforward
- Handles form submission, validation and error messages very well
- Keeps form state local
- I'm not in mood of crying
What you're going to learn
- Create editable HTML tag using
contentEditable
prop - Formik's
useField
hook -
FieldArray
component to manage list
Getting Started
Let's create a basic component, i.e. TodoApp.jsx
, for our todo list app:
const INITIAL_TODOS = [
{ id: 1, content: "learn react", isCompleted: true },
{ id: 2, content: "learn react hooks", isCompleted: true },
{ id: 3, content: "learn formik", isCompleted: false }
];
const TodoItem = ({ content, isCompleted }) => (
<div className={["todo-item", isCompleted ? "completed" : ""].join(" ")}>
<input type="checkbox" checked={isCompleted} />
<span contentEditable={true} className="todo-text">{content}</span>
</div>
);
export const TodoApp = () => {
return INITIAL_TODOS.map(todo => (
<TodoItem
key={todo.id}
content={todo.content}
isCompleted={todo.isCompleted}
/>
));
};
We've 3 todo items along with checkboxes and their content, a checkbox shows if a todo item is complete or not.
Everything is same old React except contentEditable
prop which is doing some magic, right? Well, it basically make content of an HTML tag editable whether it's text or anything else. We'll see it's real usage in next couple of code snippets.
Let's add some basic styling for todo items:
.todo-item {
display: flex;
border: 1px dashed #999;
margin: 5px 0;
padding: 5px;
}
.todo-item.completed {
text-decoration: line-through;
background: #80eec5;
}
.todo-text {
flex-grow: 1;
margin-left: 10px;
min-height: 20px;
/* Removes outline when using contentEditable */
outline: none;
overflow: hidden;
word-break: break-word;
}
The one with Formik
Run yarn add formik
or npm i --save formik
in your project repo.
We're going to wrap our todo items with Formik
.
import { Formik } from "formik";
export const TodoApp = () => (
<Formik initialValues={{ todos: INITIAL_TODOS }}>
{formikProps => {
const { values } = formikProps;
return values.todos.map((todo, index) => (
<TodoItem key={todo.id} index={index} />
));
}}
</Formik>
);
Nothing has happened just yet in actual but we've successfully integrated formik with our tiny TodoApp
.
The one with useField
We've to change TodoItem
component now as we're passing index
of the array in props.
import { useField } from "formik";
const TodoItem = ({ index }) => {
// Binding `isCompleted` using index of todos array
const [completedField] = useField({ name: `todos[${index}].isCompleted`, type: "checkbox" });
// Binding `content` using index of todos array
const [contentField, contentMeta, contentHelpers] = useField(`todos[${index}].content`);
const onContentInput = e => {
contentHelpers.setValue(e.currentTarget.innerText);
};
const onContentBlur = () => {
contentHelpers.setTouched(true);
};
return (
<div
className={["todo-item", completedField.value ? "completed" : ""].join(" ")}
>
<input
type="checkbox"
name={completedField.name}
checked={completedField.checked}
onChange={({ target }) => {
completedHelpers.setValue(target.checked);
// As type=checkbox doesn't call onBlur event
completedHelpers.setTouched(true);
}}
/>
<span
contentEditable={true}
className={"todo-text"}
onInput={onContentInput}
onBlur={onContentBlur}
>
{/*
* We must set html content through `contentMeta.initialValue`,
* because `contentField.value` will be updated upon `onChange | onInput`
* resulting in conflicts between states of content. As 1 will be managed by
* React and other with contentEditable prop.
*/}
{contentField.value}
</span>
</div>
);
};
Custom hooks are now part of formik >= v2
, useField
hook returns a 3-tuple (an array with three elements) containing FieldProps
, FieldMetaProps
and FieldHelperProps
. It accepts either a string of a field name or an object as an argument. The object must at least contain a name
key. You can read more about useField
here.
The one with FieldArray
Enough with the static data, let's dig a bit deeper and create an Add button for dynamically creating todo items. To do that we can make use of FieldArray
. 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, i.e. todos
. FieldArray
will then give you access to array helper methods via render props.
Common array helper methods:
-
push: (obj: any) => void
: Add a value to the end of an array -
swap: (indexA: number, indexB: number) => void
: Swap two values in an array -
move: (from: number, to: number) => void
: Move an element in an array to another index -
remove<T>(index: number): T | undefined
: Remove an element at an index of an array and return it
To read more about FieldArray
visit official documentation.
import { Form, Formik, FieldArray } from "formik";
export const TodoApp = () => (
<Formik initialValues={{ todos: [] }}>
<Form>
{/* Pass name of the array, i.e. `todos` */}
<FieldArray name="todos">
{({ form, ...fieldArrayHelpers }) => {
const onAddClick = () => {
fieldArrayHelpers.push({
id: values.todos.length,
content: "",
isCompleted: false
});
};
return (
<React.Fragment>
<button onClick={onAddClick}>Add Item</button>
{form.values.todos.map(({ id }, index) => (
<TodoItem key={id} index={index} />
))}
</React.Fragment>
);
}}
</FieldArray>
</Form>
</Formik>
);
There you go, it was that simple, you've a working Todo app.
You can add more features like removing the completed items but that's totally up to you.
...
const onRemoveClick = () => {
form.setFieldValue(
"todos",
form.values.todos.filter(todo => !todo.isCompleted)
);
};
<button onClick={onRemoveClick}>Remove</button>
...
I've skipped the validation part in this article as it was pretty simple but it's all implemented in the sandbox embedded below:
Most of the documentation part is taken from formik's official documentation and big thanks to Jared Palmer for all of the efforts.
Top comments (1)
Great tutorial! I now understand more about Formik.