npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-hook

Quick 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 your setFormData function and returns a simpler updater
  • updateFormData("name", e.target.value) updates just the name field, 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 of prev
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 name

Why 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 field

But 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