Skip to content

Tags collection uses side effect in select function to write uncategorized counts #578

@brendanlong

Description

@brendanlong

This is related to #580 and a PR fixing this should target the tanstack-db branch.

Problem

In src/lib/collections/tags.ts:60-76, the tags collection's select function writes uncategorized counts to the counts collection as a side effect:

select: (data: TagsListResponse) => {
  // Side effect: write uncategorized counts to the counts collection
  const existing = countsCollection.get("uncategorized");
  if (existing) {
    countsCollection.update("uncategorized", (draft) => {
      draft.total = data.uncategorized.feedCount;
      draft.unread = data.uncategorized.unreadCount;
    });
  } else {
    countsCollection.insert({
      id: "uncategorized",
      total: data.uncategorized.feedCount,
      unread: data.uncategorized.unreadCount,
    });
  }

  return data.items;
},

Why this is fragile

  1. select can be called multiple times by TanStack Query (e.g., on re-renders, cache reads). While the logic is idempotent (insert-or-update), side effects in select violate the expected contract that select is a pure transformation.

  2. Implicit ordering dependency: The counts collection must be created before the tags collection. This is currently handled correctly in createCollections() with a comment, but it's a hidden invariant.

  3. Mixing concerns: The tags collection is responsible for its own data and for populating a different collection's data. This makes the data flow harder to trace.

Suggestion

Move the uncategorized count update out of select and into a more explicit location. Some options:

  • Wrap the tags fetch so it returns items and writes uncategorized counts as two separate steps (e.g., a callback after the queryFn resolves)
  • Use a separate lightweight query for uncategorized counts (could be part of the existing entries.count prefetch)
  • Use queryClient.getQueryCache().subscribe() to watch for tags.list updates and write uncategorized counts in response
  • Split the API response — have tags.list return only tags, and uncategorized counts come from a different endpoint that seeds the counts collection directly

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions