npm package: @rmnddesign/form-hook
Description: A tiny type-safe React hook for updating form state fields
Author: rmnddesign
Latest version: 1.0.1

# @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:

```tsx
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: "john@example.com" }))

// And again for age:
setFormData(prev => ({ ...prev, age: 25 }))
```

This gets tedious fast. This hook simplifies it to:

```tsx
updateFormData("name", "John")
updateFormData("email", "john@example.com")
updateFormData("age", 25)
```

## Installation

```bash
npm install @rmnddesign/form-hook
```

## Quick Start

```tsx
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

```tsx
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

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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

```tsx
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

```tsx
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:

```tsx
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`

```tsx
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:

```tsx
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:

```tsx
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