@rmnddesign/form-hook
v1.0.1
Published
A tiny type-safe React hook for updating form state fields
Downloads
196
Readme
@rmnddesign/form-hook
A tiny type-safe React hook for updating form state fields.
The Problem
When you have a form with multiple fields stored in a single useState object, updating one field requires this awkward pattern:
const [formData, setFormData] = useState({
name: "",
email: "",
age: 0,
})
// To update just the name, you have to do this every time:
setFormData(prev => ({ ...prev, name: "John" }))
// And again for email:
setFormData(prev => ({ ...prev, email: "[email protected]" }))
// And again for age:
setFormData(prev => ({ ...prev, age: 25 }))This gets tedious fast. This hook simplifies it to:
updateFormData("name", "John")
updateFormData("email", "[email protected]")
updateFormData("age", 25)Installation
npm install @rmnddesign/form-hookQuick Start
import { useState } from "react"
import { useFormDataUpdater } from "@rmnddesign/form-hook"
function SignupForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
})
// Create the updater from your setFormData function
const updateFormData = useFormDataUpdater(setFormData)
return (
<form>
<input
type="text"
value={formData.name}
placeholder="Name"
onChange={(e) => updateFormData("name", e.target.value)}
/>
<input
type="email"
value={formData.email}
placeholder="Email"
onChange={(e) => updateFormData("email", e.target.value)}
/>
<input
type="password"
value={formData.password}
placeholder="Password"
onChange={(e) => updateFormData("password", e.target.value)}
/>
<p>Hello, {formData.name || "stranger"}!</p>
</form>
)
}What's happening here:
useFormDataUpdater(setFormData)takes yoursetFormDatafunction and returns a simpler updaterupdateFormData("name", e.target.value)updates just thenamefield, keeping all other fields unchanged- TypeScript knows which fields exist and what types they should be
Common Examples
Basic form fields
const [formData, setFormData] = useState({
firstName: "",
lastName: "",
age: 0,
subscribed: false,
})
const updateFormData = useFormDataUpdater(setFormData)
// Update a string
updateFormData("firstName", "John")
// Update a number
updateFormData("age", 25)
// Update a boolean
updateFormData("subscribed", true)Using with different input types
const [formData, setFormData] = useState({
name: "",
age: 0,
color: "red",
newsletter: false,
})
const updateFormData = useFormDataUpdater(setFormData)
// Text input
<input
type="text"
value={formData.name}
onChange={(e) => updateFormData("name", e.target.value)}
/>
// Number input
<input
type="number"
value={formData.age}
onChange={(e) => updateFormData("age", Number(e.target.value))}
/>
// Select dropdown
<select
value={formData.color}
onChange={(e) => updateFormData("color", e.target.value)}
>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
// Checkbox
<input
type="checkbox"
checked={formData.newsletter}
onChange={(e) => updateFormData("newsletter", e.target.checked)}
/>Nested objects
For nested objects, you can pass a callback function that receives the current value:
const [formData, setFormData] = useState({
name: "",
address: {
street: "",
city: "",
zip: "",
},
})
const updateFormData = useFormDataUpdater(setFormData)
// Update nested field using a callback
// The callback receives the current address object
updateFormData("address", (prevAddress) => ({
...prevAddress, // keep existing address fields
city: "Amsterdam", // update just the city
}))Creating a scoped updater for nested objects
If you're updating many fields in a nested object, create a dedicated updater for it:
const [formData, setFormData] = useState({
name: "",
address: {
street: "",
city: "",
zip: "",
},
})
const updateFormData = useFormDataUpdater(setFormData)
// Create an updater specifically for the address object
const updateAddress = useFormDataUpdater(
(fn) => updateFormData("address", fn)
)
// Now you can update address fields directly
updateAddress("street", "123 Main St")
updateAddress("city", "Amsterdam")
updateAddress("zip", "1234AB")Working with arrays
For arrays, use a callback function to create a new array:
const [formData, setFormData] = useState({
name: "",
tags: [] as string[],
items: [] as { id: number; text: string }[],
})
const updateFormData = useFormDataUpdater(setFormData)
// Add an item to an array
updateFormData("tags", (prevTags) => [...prevTags, "new-tag"])
// Remove an item by index
updateFormData("tags", (prevTags) => prevTags.filter((_, index) => index !== 2))
// Remove an item by value
updateFormData("tags", (prevTags) => prevTags.filter((tag) => tag !== "old-tag"))
// Update a specific item in an array of objects
updateFormData("items", (prevItems) =>
prevItems.map((item, index) =>
index === 1 ? { ...item, text: "Updated text" } : item
)
)
// Add an object to an array
updateFormData("items", (prevItems) => [
...prevItems,
{ id: Date.now(), text: "New item" },
])Complete form example
import { useState } from "react"
import { useFormDataUpdater } from "@rmnddesign/form-hook"
type ContactForm = {
name: string
email: string
message: string
priority: "low" | "medium" | "high"
subscribe: boolean
}
function ContactPage() {
const [formData, setFormData] = useState<ContactForm>({
name: "",
email: "",
message: "",
priority: "medium",
subscribe: false,
})
const updateFormData = useFormDataUpdater(setFormData)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
console.log("Submitting:", formData)
// Send to your API...
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name</label>
<input
type="text"
value={formData.name}
onChange={(e) => updateFormData("name", e.target.value)}
/>
</div>
<div>
<label>Email</label>
<input
type="email"
value={formData.email}
onChange={(e) => updateFormData("email", e.target.value)}
/>
</div>
<div>
<label>Message</label>
<textarea
value={formData.message}
onChange={(e) => updateFormData("message", e.target.value)}
/>
</div>
<div>
<label>Priority</label>
<select
value={formData.priority}
onChange={(e) => updateFormData("priority", e.target.value as ContactForm["priority"])}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div>
<label>
<input
type="checkbox"
checked={formData.subscribe}
onChange={(e) => updateFormData("subscribe", e.target.checked)}
/>
Subscribe to newsletter
</label>
</div>
<button type="submit">Send</button>
</form>
)
}API Reference
The hook
const updateFormData = useFormDataUpdater(setFormData)Parameter:
| Parameter | Type | Description |
|-----------|------|-------------|
| setFormData | (updater: (prev: T) => T) => void | The setState function from your useState hook |
Returns:
An updater function with this signature:
updateFormData(fieldName, newValue)
// or
updateFormData(fieldName, (prevValue) => newValue)| Parameter | Type | Description |
|-----------|------|-------------|
| fieldName | keyof T | The name of the field to update (TypeScript autocompletes this!) |
| newValue | T[fieldName] or (prev: T[fieldName]) => T[fieldName] | The new value, or a function that receives the current value and returns the new value |
TypeScript
The hook is fully type-safe:
- Field names are autocompleted - TypeScript knows which fields exist in your form
- Values are type-checked - You can't accidentally set a string to a number field
- Callback types are inferred - When using
(prev) => newValue, TypeScript knows the type ofprev
const [formData, setFormData] = useState({
name: "", // string
age: 0, // number
active: false, // boolean
})
const updateFormData = useFormDataUpdater(setFormData)
updateFormData("name", "John") // OK
updateFormData("name", 123) // Error: number is not assignable to string
updateFormData("age", 25) // OK
updateFormData("age", "twenty") // Error: string is not assignable to number
updateFormData("typo", "value") // Error: "typo" is not a valid field nameWhy not just use separate useState calls?
You could have a separate useState for each field:
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const [age, setAge] = useState(0)
// ... and so on for every fieldBut this gets messy when:
- You have many fields
- You need to pass all form data somewhere (like to an API)
- You want to reset the entire form
- You need to validate fields together
A single form state object is cleaner:
const [formData, setFormData] = useState({ name: "", email: "", age: 0 })
const updateFormData = useFormDataUpdater(setFormData)
// Easy to pass around
submitToAPI(formData)
// Easy to reset
setFormData({ name: "", email: "", age: 0 })License
MIT
