# Step 11 — The React Hook: `useTags` with TanStack Query

Step 10 gave us `tagsApi` — four functions that return promises. `vibe-frontend/src/hooks/useTags.ts` is where those promises become reactive, cached, component-ready state, using the TanStack Query fundamentals previewed back in Step 6.

## Reads: `useQuery`

```ts
export function useTags(teamId?: string) {
  const queryClient = useQueryClient();

  const tagsQuery = useQuery({
    queryKey: ['tags', teamId],
    queryFn: () => tagsApi.list({ team_id: teamId }),
    enabled: !!teamId,
    staleTime: 5 * 60 * 1000,
    gcTime: 15 * 60 * 1000,
  });
  // ...
}
```

Exactly the shape from Step 6, now with a real `queryFn`: `() => tagsApi.list({ team_id: teamId })`. `gcTime` (garbage-collect time) controls how long an *unused* cache entry is kept around before being dropped — separate from `staleTime`, which controls when a still-cached entry is considered outdated and eligible for a background refetch.

## Writes: `useMutation` + optimistic-ish cache updates

```ts
const createTagMutation = useMutation({
  mutationFn: (data: CreateTag) => tagsApi.create(data),
  onSuccess: (newTag) => {
    queryClient.setQueryData<Tag[]>(['tags', teamId], (old) => {
      if (!old) return [newTag];
      return [...old, newTag].sort((a, b) => a.tag_name.localeCompare(b.tag_name));
    });
  },
});
```

Rather than refetch the whole tag list after creating one tag, `onSuccess` directly patches the existing cache entry (`queryClient.setQueryData`) by appending the new tag and re-sorting — the UI updates instantly with zero extra network round-trip. `updateTagMutation` and `deleteTagMutation` follow the identical shape: call the matching `tagsApi` method, then surgically update the cached array (`map` to replace, `filter` to remove) instead of invalidating and refetching.

```ts
const deleteTagMutation = useMutation({
  mutationFn: (tagId: string) => tagsApi.delete(tagId),
  onSuccess: (_, tagId) => {
    queryClient.setQueryData<Tag[]>(['tags', teamId], (old) => old?.filter((t) => t.id !== tagId) ?? []);
    queryClient.invalidateQueries({ queryKey: ['task-tags'], refetchType: 'none' });
  },
});
```

Deleting a tag also invalidates the *separate* `['task-tags']` cache (tags-attached-to-tasks) — because deleting a tag can affect data that a completely different query owns. `refetchType: 'none'` marks that cache stale **without** forcing an immediate refetch; it'll refetch next time something actually reads it.

## A hook that returns exactly what a component needs

```ts
return {
  tags: tagsQuery.data ?? [],
  isLoading: tagsQuery.isLoading,
  error: tagsQuery.error,
  createTag: createTagMutation.mutateAsync,
  updateTag: updateTagMutation.mutateAsync,
  deleteTag: deleteTagMutation.mutateAsync,
  isCreating: createTagMutation.isPending,
  isUpdating: updateTagMutation.isPending,
  isDeleting: deleteTagMutation.isPending,
};
```

This is the same "purpose-built hook, not a raw store" principle from Step 5's `useTaskStopping` — a component that calls `useTags(teamId)` never touches `tagsApi`, `queryClient`, or cache keys directly. It gets a flat object: data, loading flags, and plain async functions to call.

## Checkpoint

Notice `tags: tagsQuery.data ?? []` — a component using `useTags` never has to check "is `tags` undefined?" before mapping over it; the hook itself normalizes the "no data yet" case to an empty array. This is a small but deliberate ergonomics decision that every consumer benefits from once, instead of every consumer repeating a null-check.

Continue to **Step 12 — The Component + a DevTools Network Trace**, the last step, where `useTags` gets consumed by `TagEditDialog` and we watch the whole chain fire in the browser.
