# Step 12 — The Component + a DevTools Network Trace

Last step. `TagEditDialog.tsx` is where every layer from Steps 7–11 finally reaches the screen. Then we'll open Chrome DevTools and *watch* the whole chain fire for a real save.

## The component: form state + `tagsApi`, directly

```tsx
const TagEditDialogImpl = NiceModal.create<TagEditDialogProps>(({ tag }) => {
  const [formData, setFormData] = useState({ tag_name: '', content: '' });
  const [saving, setSaving] = useState(false);
  const isEditMode = Boolean(tag);

  const handleSave = async () => {
    if (!formData.tag_name.trim()) {
      setError(t('settings.general.tags.dialog.errors.nameRequired'));
      return;
    }
    setSaving(true);
    try {
      if (isEditMode && tag) {
        await tagsApi.update(tag.id, { tag_name: formData.tag_name, content: formData.content || null });
      } else {
        await tagsApi.create({ tag_name: formData.tag_name, content: formData.content });
      }
      modal.resolve('saved');
      modal.hide();
    } catch (err: unknown) {
      setError(getErrorMessage(err) || t('settings.general.tags.dialog.errors.saveFailed'));
    } finally {
      setSaving(false);
    }
  };
  // ...
});
```

Interesting choice worth noticing: this dialog calls `tagsApi.update`/`.create` **directly**, not through `useTags`'s mutation functions. That's a legitimate variation, not a violation of the pattern — `useTags` exists for components that need cache-synced *lists* of tags (a settings page showing every tag), while a modal that only needs to fire one write and report success/failure back to its caller (`modal.resolve('saved')`) is simpler calling the API client straight. Either is valid; the type contracts from Steps 9–10 (`CreateTag`, `UpdateTag`, `Tag`) are what make both call sites safe.

`isEditMode = Boolean(tag)` is the same "one component, two modes" shape you'd expect from `AgentModeBadge`'s `mode` prop in Step 2 — here driven by whether an existing `Tag` was passed in, rather than an explicit union.

## Now: watch it happen in DevTools

With `pnpm dev` running (Step 3), open any page with a tag editor, open Chrome DevTools → **Network** tab, filter by `Fetch/XHR`, and click "Edit" on a tag.

1. Type a change and hit Save. You'll see a single request appear: `PUT /api/tags/<uuid>`.
2. Click that request → **Headers** tab. Confirm:
   - `Content-Type: application/json` and an `Authorization: Bearer ...` header — both added invisibly by `makeRequest` (Step 10).
3. Click **Payload** (or **Request**). The body is exactly your `UpdateTag` shape from Step 9: `{"tag_name": "...", "content": "..."}` — no extra fields, because `TagEditDialog` builds precisely that type.
4. Click **Response**. You'll see the backend's envelope from Step 8's `ApiResponse::success(tag)` — `{"success": true, "data": { "id": "...", "tag_name": "...", ... }}` — matching `Tag` field-for-field.
5. Switch to the **React Query DevTools** panel (if enabled in your local build) and watch the `['tags', teamId]` query — if this dialog were wired through `useTags` instead of a direct call, you'd see its cache entry update the instant `onSuccess` fires, with no visible refetch.

That single `PUT` request is the entire tutorial in miniature: a click in `TagEditDialog.tsx` (Step 12) → `tagsApi.update` (Step 10) → an Axum handler (Step 8) → `TagRepository::update` (Step 7) → a row change in Postgres → a `Tag`-shaped JSON response, decoded back through `shared/types.ts` (Step 9) into the exact same TypeScript type the dialog started with.

## Where to go from here

You've traced one real feature end-to-end. The same five-layer trace — DB repository → Axum route → `shared/types.ts` → API client → React hook/component — applies to *every* feature in this codebase. Next time you need to understand or extend one, start at whichever layer is closest to your change and walk outward using this same method.
