Components, hooks & utilities for creating and managing delightfully simple form experiences in React.
React-f3 promises to take a step back, rethink what a form actually is (and importantly, what it _is not_), and makes developing these forms a delightful yet robust experience.
tsx
import { Form, useForm } from "react-f3";
import z from "zod";
const FormSchema = z.object({
email: z.string().refine((x) => x.includes("@")),
password: z.string().min(8),
});
const Issue = ({ issue }: { issue: z.core.$ZodIssue }) => (
{issue.message}
);
const UserForm = () => {
const form = useForm({
schema: FormSchema,
onSubmit: async (data) => {
// Simulate network, wait a second
await new Promise((res) => setTimeout(res, 1000));
// Validated and transformed data!
console.log(data);
},
});
return (
);
};
`
That's it. No default state in the form, as that's not the place where your defaults should be stored. Notice how you can use an error chain with a render function to conditionally render when there is an issue matching the issue chain.
tsx
// Normally, these would come from some backend query.
// Consider using something like react-query for this!
const defaults = [
"Wake up",
"Drink coffee",
"Do some programming",
"Go to bed",
] as const;
// HTML5 checkboxes can have custom values, but yield
// "on" by default.
const createCheckboxSchema = (values: string | string[] = "on") =>
z.enum([values].flat());
const Schema = z.object({
todos: z
.record(
z.string(),
z.object({
todo: z.string().trim().nonempty(),
done: createCheckboxSchema(),
})
)
// Note the transform here, we just want an array
// when submitting.
.transform(Object.values),
});
const App = () => {
const [isEditMode, setIsEditMode] = useState(false);
const form = useForm({
schema: Schema,
onSubmit: async (data) => {
// Wait a second, simulate network
await new Promise((res) => setTimeout(res, 1000));
console.log(data);
},
});
const todos = useFieldset(
form,
// Try removing the isEditMode check and see what happens!
isEditMode && defaults
);
const [done, setDone] = useState([]);
return (
{JSON.stringify(form.validation?.error)}
$3
Click here to open the example using react-select
Because we can't rely on the FormData API to distinguish between single and multi entry fields, we have to do some mapping of values to allow react-f3 to pick up the inputs in the form data.
`tsx
import { useMemo, useState, type ReactNode } from "react";
import { Form, useForm, type FieldGetter } from "react-f3";
import ReactSelect, { type GroupBase, type Props } from "react-select";
import z from "zod";
const hobbies = [
"Programming",
"Thinking about programming",
"Making music",
"Sleeping",
];
const FormSchema = z.object({
hobbies: z.array(z.enum(hobbies)).min(2),
});
const Issue = ({ issue }: { issue: z.core.$ZodIssue }) => (
{issue.message}
);
type SelectProps = Readonly<{
name: (n: number) => FieldGetter;
}>;
type Option = Readonly<{
label: ReactNode;
value: T;
}>;
const Select = <
T,
IsMulti extends boolean,
Group extends GroupBase `
As you can see, you can pass the FieldGetter directly to a Select component, which in turn can render hidden inputs per value using the getter with an index.
The type juggling around the Select component is just to treat single and multi inputs the same. Single input selects will work without issue, as they don't yield array values.
FAQ
How do you store default state?
That's the neat part: you don't! React is specifically made to track your application state and render the UI accordingly, no need to do this in the form as well.
_But then what would we recommend instead?_
Often times, your default state lives on the server, so it would only be logical to simply keep it there. Consider using a library like react-query for fetching your defaults, and leverage React's components to just pass that data through. Rendering defaults is as simple as providing the defaultValue prop to an !
`tsx
const Example = () => {
const url = "https://example.com/api/v1/things?pagesize=10&page=3";
const query = useQuery({
queryKey: [url],
queryFn: () => fetch(url)
});
const form = useForm({
schema: /.../,
onSubmit: /.../,
});
if (query.isLoading) {
// Please use a better loading state than this 😉
return "Loading...";
}
}
`
If your state is fully local, then it's even easier: render an input with a defaultValue pointing to that state.
How do you render dependent fields? (aka how to read the form's current state)
React-F3 does not store any form state, and that's by design. While it could be a nice quality-of-life feature, it's also the main reason other form libraries are either really slow, or why they are designed to circumvent React through subscriptions and/or black magic and Proxy objects. If you want to track state for any specific fields, just do that; add a useState hook and update it onChange, simple as that!
`tsx
const Example = () => {
const form = useForm({
schema: /.../,
onSubmit: /.../,
});
const [fieldValue, setFieldValue] = useState("");
}
`
HTML's native form api provides several ways of reading any input's current value, and react-f3 exports some utilities for easily integrating those with React. If you just want the current value of a field in your form, you can use the useField() hook.
`tsx
const Example = () => {
const form = useForm({
schema: /.../,
onSubmit: /.../,
});
const fieldValue = useField({
form,
name: form.fields.fieldName()
});
}
`
Why is there a <Form/> component?
It's a small helper component that wraps your forms in a which gets disabled while your form is submitting. A neat feature of HTML's native form api is that any input that is a child of a disabled fieldset will also get disabled! There is absolutely no need to use the component, but it is a good basic user experience.
Do you have any unanswered questions? Create an issue! If it gets enough 👍s it will get added here.
What does F3 stand for?
It stands for form, form, form, of course! This is because, when using this package, your forms will often start with the following:
`tsx
`
Featured
I'd love to hear about all of your creative forms using this library. If you want to share, get in touch and your creations could be featured here!
Contributing
Don't hesitate to create an issue if you run into any problems using react-f3. Pull requests are also very welcome!
Thanks
Shout out to @esamattis' react-zorm for being a big inspiration, but it's sadly no longer maintained.
Big thanks to @colinhacks for creating zod, and featuring react-f3` in the zod ecosystem!