Skip to main content

Two steps: load, then save

Updating a record means: load the current data into a form, let the user edit it, then send the changes back to the API.
The companion repo keeps update simpler and reuses UserCreate for PUT /api/users/{id} (full replacement). This lesson introduces a separate UserUpdate model with optional fields to teach the common “partial update shape” pattern.
class UserUpdate(BaseModel):
    name: str | None = None
    email: str | None = None

@app.put("/api/users/{user_id}")
def update_user(user_id: int, updates: UserUpdate):
    user = find_user(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")

    if updates.name is not None:
        user["name"] = updates.name
    if updates.email is not None:
        user["email"] = updates.email

    return user

The edit form component

The key difference from the Create form: initialize state with the existing data.
import { useState } from 'react';
import { updateUser } from '../api/users';

function EditUserForm({ user, onSave, onCancel }) {
  const [formData, setFormData] = useState({
    name: user.name,
    email: user.email,
  });
  const [error, setError] = useState(null);
  const [saving, setSaving] = useState(false);

  function handleChange(e) {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setError(null);
    setSaving(true);

    try {
      const updatedUser = await updateUser(user.id, formData);
      onSave(updatedUser); // Tell parent about the update
    } catch (err) {
      setError(err.message);
    } finally {
      setSaving(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" value={formData.name} onChange={handleChange} />
      <input name="email" value={formData.email} onChange={handleChange} />

      {error && <p className="error">{error}</p>}

      <button type="submit" disabled={saving}>
        {saving ? "Saving..." : "Save Changes"}
      </button>
      <button type="button" onClick={onCancel}>
        Cancel
      </button>
    </form>
  );
}
Notice:
  • State initialized from user prop — form starts with current values
  • onSave callback — parent receives the updated object from the API
  • onCancel callback — lets the user exit edit mode without saving
  • Cancel is type="button" — prevents it from submitting the form

Toggle between view and edit mode

The parent component switches between displaying data and showing the edit form:
function UserCard({ user, onUpdate }) {
  const [editing, setEditing] = useState(false);

  function handleSave(updatedUser) {
    onUpdate(updatedUser);
    setEditing(false);
  }

  if (editing) {
    return (
      <EditUserForm
        user={user}
        onSave={handleSave}
        onCancel={() => setEditing(false)}
      />
    );
  }

  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={() => setEditing(true)}>Edit</button>
    </div>
  );
}
function UserDashboard() {
  const [users, setUsers] = useState([]);

  function handleUpdate(updatedUser) {
    setUsers(prev =>
      prev.map(u => u.id === updatedUser.id ? updatedUser : u)
    );
  }

  return (
    <div>
      {users.map(user => (
        <UserCard
          key={user.id}
          user={user}
          onUpdate={handleUpdate}
        />
      ))}
    </div>
  );
}
The state update uses .map() — replace the old user with the updated one, leave everything else unchanged.
prev.map(u => u.id === updatedUser.id ? updatedUser : u) is the standard pattern for updating one item in a list. It creates a new array where only the matching item is replaced. You’ll use this pattern constantly.

Optimistic vs pessimistic updates

Pessimistic (wait for server, then update UI):
async function handleSave(formData) {
  setSaving(true);
  try {
    const updated = await updateUser(user.id, formData); // Wait for API
    onUpdate(updated); // Then update UI
  } catch (err) {
    setError(err.message); // Show error, UI unchanged
  } finally {
    setSaving(false);
  }
}
Optimistic (update UI immediately, roll back on error):
async function handleSave(formData) {
  const previousUser = { ...user }; // Save backup

  // Update UI immediately (optimistic)
  onUpdate({ ...user, ...formData });
  setEditing(false);

  try {
    await updateUser(user.id, formData); // Confirm with server
  } catch (err) {
    onUpdate(previousUser); // Roll back on failure
    setError("Save failed. Your changes were reverted.");
    setEditing(true);
  }
}
ApproachUXComplexityWhen to use
PessimisticSpinner, then updateSimpleMost operations (start here)
OptimisticInstant, may roll backMore complexFrequent actions (likes, toggles)
Start with pessimistic updates. They’re simpler and always correct. Only switch to optimistic updates for actions where the slight delay feels sluggish — like toggling a checkbox or liking a post.

What’s next?

You can create, read, and update. The last CRUD operation: deleting records with confirmation.

Delete operation

Remove records with confirmation and proper error handling