react-simple-formkit
v2.0.4
Published
Support form handling simply. Configuration is simple, fast, and efficient.
Downloads
198
Maintainers
Readme
React Simple FormKit
Supports managing forms as uncontrolled. State updates only when watched. Simple and quick to configure with outstanding efficiency.
Table of Contents
- Peer dependencies
- Features
- Quick start
- Core concepts
- Manage values
- Manage states
- Manage errors
- APIs
- Examples
- Contact
Peer dependencies
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},Features
- Lightweight: only ~15KB
- Easy setup: simple and quick configuration
- Optimized for uncontrolled forms: no unnecessary re-renders
- Comprehensive form management:
Quick start
const { control } = useForm();
const handleSubmit = (data) => {
alert(JSON.stringify(data));
};
return (
<Form control={control} onSubmit={handleSubmit} onChange={console.log}>
<button type="submit">Submit</button>
<input required name="email" placeholder="email" />
<input required name="password" type="password" placeholder="password" />
{Array.from({ length: 10 }).map((_, index) => (
<input name={`input${index + 1}`} placeholder={`input ${index + 1}`} />
))}
</Form>
);Core concepts
Modes of input field
Uncontrolled:
Basic HTML form elements whose value is natively managed by the browser. Such as<input type="text">, <input type="checkbox">, <select>, or <textarea>.
Note: Or also UI library components that are simple wrappers around these native elements that still follow the browser’s standard behavior
Controlled:
Advanced UI components with complex INPUT AND OUTPUT . For example, a multiple select component expects an array as its input/output, but your form state might need to store the value as a string.
By using Controller, you can transform the value in both directions:
- On
input: split the stored string into an array to pass to the UI component. - On
change: join the selected array back into a string before updating the form state.
Note: Or whenever you want to control the value of a field (e.g. by calling
actions.setValue), you must wrap that field with aController. WithoutController, the field will not respond to external value changes.
Example:
<Controller
name="multipleSelect"
render={({ value = "", onChange, name }) => {
return (
<Select
multiple
name={name}
value={value.split(",")}
onChange={(e) => {
const value = e.target.value;
// value as array by default but on autofill value as string
onChange(typeof value === "string" ? value : value.join(","));
}}
>
<MenuItem value="10">Ten</MenuItem>
<MenuItem value="20">Twenty</MenuItem>
<MenuItem value="30">Thirty</MenuItem>
</Select>
);
}}
/>Captured
Advanced UI components with complex OUTPUT ONLY
In these cases, the field is captured:
- You only listen to its value changes through a custom
onChange. - You can sync the final value into the form when needed (via
actions.setValue).
Example:
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker name="date" onChange={(newValue) => actions.setValue("date", newValue.format("MM/DD/YYYY"))} />
</LocalizationProvider>const { control, actions } = useForm();
return (
<Form control={control} onChange={console.log}>
{/* Uncontrolled */}
<input name="number1" placeholder="Enter a number" />
{/* Controlled by Controller. And the value at this field can be controlled by actions.setValue() */}
<Controller
name="multipleSelect"
render={({ value = "", onChange, name }) => {
return (
<Select
multiple
name={name}
value={value.split(",")}
onChange={(e) => {
const value = e.target.value;
// value as array by default but on autofill value as string
onChange(typeof value === "string" ? value : value.join(","));
}}
>
<MenuItem value="10">Ten</MenuItem>
<MenuItem value="20">Twenty</MenuItem>
<MenuItem value="30">Thirty</MenuItem>
</Select>
);
}}
/>
{/* Captured: This datepicker component changes value immediately with a click with custom onChange */}
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker name="date" onChange={(newValue) => actions.setValue("date", newValue.format("MM/DD/YYYY"))} />
</LocalizationProvider>
<button type="button" onClick={() => alert(JSON.stringify(actions.getValues()))}>
Get value
</button>
<button type="submit">Submit</button>
</Form>
)Note*: All fields in the form must follow one of the three modes above to ensure the form works correctly
Watching for updates
State updates only when observed via watch() or useWatch(), or by tracking changes through the Form component's onChange callback.
watch()will trigger a re-render at the form level.useWatch()will trigger a re-render only in the component where it is called.onChangeis just a handler callback that is called when field values change.
Manage values
Get value
// watch
const fieldName = watch("fieldName")
const fieldName2 = watch("fieldName2")
// Or
const { fieldName2, fieldName } = watch(["fieldName", "fieldName2"])
// Or
const values = watch("values")
// useWatch
useWatch({ name: "fieldName" })
useWatch({ name: ["fieldName", "fieldName2"] })
useWatch({ compute: (values) => values.number > 10 ? true : false })
// actions.getValues()
const { actions } = useForm()
console.log(actions.getValues())Set value
actions.setValue() can help to control value of a field. But that field must be controlled by Controller.
const { control, actions, watch } = useForm()
const number = watch("number")
return (
<Form control={control} onChange={console.log}>
<Controller
name="number"
render={({ name, value, onChange }) => (
<input name={name} value={value} onChange={(e) => onChange(e.target.value)} />
)}
/>
<button type="button" onClick={() => actions.setValue("number", Number(number || 0) + 1 )}>
Increase
</button>
<button type="submit" disabled={!isDirty}>
Submit
</button>
</Form>
)Default values and reset
const dummyFields = Array.from({ length: 10 }).reduce(
(acc, _, index) => ({ ...acc, [`input${index + 1}`]: `input${index + 1}` }),
{}
);
const { control, watch, actions } = useForm({ defaultValues: dummyFields });
const handleSubmit = async (newValues) => {
// update to server
await new Promise(res => setTimeout(res, 1000))
// reset with new defaultValues
actions.reset(newValues)
}
return (
<Form control={control} onSubmit={handleSubmit} onChange={console.log}>
{Object.keys(dummyFields).map((name) => (
<input name={name} placeholder={name} />
))}
<button type="reset" disabled={!isDirty}>
Reset
</button>
<button type="submit" disabled={!isDirty}>
Submit
</button>
</Form>
);Manage states
Get field states
// watch() or useWatch()
const isFormDirty = watch("isDirty")
const { fieldName: {...}, fieldName2: {...} } = watch("fieldStates")
const { isDirty, isTouched } = watch("fieldStates.fieldName")
const isFieldDirty = watch("fieldStates.fieldName.isDirty")
const fieldCustomState = watch("fieldStates.fieldName.customState")Set field states
actions.setFieldStateProperty('fieldName', 'customState', { hello: "world" })
// This will trigger re-render at watch(), useWatch() and render function in ControllerManage errors
Get field error
// watch() or useWatch()
const isFormError = watch("isError")
const fieldError = watch("errors.fieldName")
const { fieldName, fieldName2 } = watch("errors")Set field error
// actions.setError()
const { control, actions, watch } = useForm()
const { isError, "errors.input1": input1Error } = watch(["isError", "errors.input1"])
return (
<Form control={control} onChange={console.log}>
<input name="input1" />
{input1Error && <span className="error-message">{input1Error}</span>}
<button type="button" onClick={() => actions.setError("number", "This is error message")}>
Set Error
</button>
<button type="submit" disabled={!isDirty}>
Submit
</button>
</Form>
)APIs
useForm
Generic props:
defaultValues:ObjectExampleshouldUnRegister:BooleanDefault is false,shouldConvertNumber:BooleanDefault is falsenumberFields:Arrayif passed, shouldConvertNumber will set true. Lib auto load field with type number by default
Return:
control: contains methods and utilities to control the form.watch(name):FunctionExampleactionsis an object that contains utilities
Form
Generic props:
control: received from useForm()onSubmit:(currentValues) => {}called when the form is submitted via a button withtype='submit'onChange:(name, value, currentValues) => {}called when any field value changes
Controller
Generic props:
namedefaultValuerender({name, value, onChange, customState, setCustomState})
useController
Generic props:
namedefaultValue
Return: everything in render function above
useWatch
Generic props:
name:String | Array- compute:
Functionthat will calculate from form values and return a value. It will make re-render when the result changes
useFormContext
Return:
watchactions
Examples
- https://codesandbox.io/p/sandbox/react-simple-formkit-examples-rhmhjj
Contact
For any ideas or issues, please get in touch with me at [email protected]
