# feat: migrate components to TanStack Query v6 (Phase 2) ## Summary This PR migrates 4 components from the custom `createInfiniteScrollResource` pattern to TanStack Query v6's `createInfiniteQuery`: - **JobSkillSelectionSidebar** - Job skill search with infinite scroll and category filtering - **SearchContent** - Search modal for weapons/characters/summons with element/rarity/proficiency filters - **User Profile Page** (`[username]/+page.svelte`) - User's teams and favorites with tab switching - **Teams Explore Page** (`teams/explore/+page.svelte`) - Public teams listing All components now use: - TanStack Query v6 infinite query pattern with thunk for reactivity - `IsInViewport` from runed for intersection-based infinite scroll - Debounced search (debounce the value, not the query) - Proper loading, error, and empty states - SSR integration with `initialData` pattern ## Updates since last revision - **Fixed duplicate key error in SearchContent.svelte** - The API can return the same item across multiple pages during infinite scroll (e.g., due to items being added/removed between page fetches). This caused Svelte's keyed each block to throw `each_key_duplicate` errors. Fixed by deduplicating results by `id` using a Map before rendering. ## Review & Testing Checklist for Human This is a medium-risk change affecting core user-facing pages. Please verify: - [ ] **Infinite scroll works on all 4 pages** - Scroll to bottom and verify more items load automatically - [ ] **No duplicate items appear in search results** - After the fix, scrolling through many pages of search results should not show duplicates - [ ] **SSR hydration** - Verify no flash of loading state on initial page load (data should be pre-rendered) - [ ] **User profile tab switching** - Test switching between "Teams" and "Favorites" tabs; verify correct data loads - [ ] **Search debouncing** - Type quickly in JobSkillSelectionSidebar and SearchContent; verify queries aren't fired on every keystroke - [ ] **Error states** - Simulate network failure and verify retry button works **Recommended Test Plan:** 1. Navigate to `/teams/explore` - verify teams load and infinite scroll works 2. Navigate to a user profile page - verify teams load, switch to favorites tab, verify favorites load 3. Open the search sidebar - search for weapons/characters/summons, scroll through many pages, verify no duplicate key errors and no duplicate items 4. Open job skill selection - search and filter skills, verify results ### Notes - The Party component mutations migration (Follow-Up Prompt 5) was deferred to a follow-up PR due to complexity - Deprecated resource classes remain in codebase for now; removal planned for separate PR - Pre-existing paraglide module errors in build are unrelated to this PR - Type assertions (`as unknown as`, `as any`) are used to handle different query result structures between favorites and parties queries **Link to Devin run:** https://app.devin.ai/sessions/5aa7ea29edf34f569f95f13acee9e0d9 **Requested by:** Justin Edmund (justin@jedmund.com) / @jedmund --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Justin Edmund <justin@jedmund.com>
7.9 KiB
TanStack Query Migration - Continuation Guide
This document provides context for continuing the TanStack Query v6 migration in hensei-web.
Migration Status
Completed (PR #441 - merged)
- Query options factories:
party.queries.ts,job.queries.ts,user.queries.ts,search.queries.ts - Mutation configurations:
party.mutations.ts,grid.mutations.ts,job.mutations.ts - SSR utilities:
withInitialData,prefetchQuery,prefetchInfiniteQuery - Example components:
JobSelectionSidebar.svelte,teams/[id]/+page.svelte
Completed (PR #442 - pending merge)
JobSkillSelectionSidebar.svelte- Job skill search with infinite scrollSearchContent.svelte- Search modal for weapons/characters/summons[username]/+page.svelte- User profile page with teams/favorites tabsteams/explore/+page.svelte- Public teams listing
Remaining Work
Follow-Up Prompt 5: Party Component Mutations
Priority: High Complexity: Large
The Party.svelte component (1535 lines) needs to be migrated to use TanStack Query mutations instead of direct service calls.
Files to modify:
src/lib/components/party/Party.svelte
Current state: Uses PartyService, GridService, ConflictService, and direct partyAdapter calls.
Target state: Use mutation hooks from:
src/lib/api/mutations/party.mutations.ts-useUpdateParty,useDeleteParty,useRemixParty,useFavoriteParty,useUnfavoriteParty,useRegeneratePreviewsrc/lib/api/mutations/grid.mutations.ts-useCreateGridWeapon,useUpdateGridWeapon,useDeleteGridWeapon, etc.src/lib/api/mutations/job.mutations.ts-useUpdatePartyJob,useUpdatePartyJobSkills,useRemovePartyJobSkill,useUpdatePartyAccessory
Recommended sub-tasks:
- 5a: Party metadata mutations - name, description, visibility using
useUpdateParty - 5b: Grid weapon mutations - add/update/delete weapons using grid mutations
- 5c: Grid character mutations - add/update/delete characters using grid mutations
- 5d: Grid summon mutations - add/update/delete summons using grid mutations
- 5e: Job and skill mutations - job selection, skill management using job mutations
Key functions to migrate in Party.svelte:
updatePartyDetails()- replacepartyService.update()withuseUpdateParty().mutate()toggleFavorite()- replacepartyService.favorite()/unfavorite()withuseFavoriteParty()/useUnfavoriteParty()remixParty()- replacepartyService.remix()withuseRemixParty()deleteParty()- replacepartyService.delete()withuseDeleteParty()handleSelectJob()- replacepartyAdapter.updateJob()withuseUpdatePartyJob()handleSelectJobSkill()- replacepartyAdapter.updateJobSkills()withuseUpdatePartyJobSkills()- Drag-drop operations - replace
gridService.moveWeapon/Character/Summon()with appropriate mutations
Follow-Up Prompt 6: Remove Deprecated Resource Classes
Priority: Low Complexity: Small Prerequisite: All components migrated away from resource classes
Files to delete:
src/lib/api/adapters/resources/search.resource.svelte.tssrc/lib/api/adapters/resources/party.resource.svelte.tssrc/lib/api/adapters/resources/job.resource.svelte.tssrc/lib/api/adapters/resources/infiniteScroll.resource.svelte.ts
Steps:
- Search for any remaining imports:
grep -r "from.*resources/" src/ - Migrate any remaining usages
- Delete the resource files
- Update any barrel exports (index.ts files)
- Run build to verify no import errors
Current blockers: InfiniteScroll.svelte still imports InfiniteScrollResource type
Patterns and Best Practices
Infinite Query Pattern
import { createInfiniteQuery } from '@tanstack/svelte-query'
import { IsInViewport } from 'runed'
// Create the query with thunk for reactivity
const query = createInfiniteQuery(() => ({
...queryOptions.list(filters),
initialData: serverData ? {
pages: [{ results: serverData.items, page: 1, totalPages: serverData.totalPages }],
pageParams: [1]
} : undefined,
initialDataUpdatedAt: 0
}))
// Flatten and deduplicate results
const rawResults = $derived(query.data?.pages.flatMap((page) => page.results) ?? [])
const items = $derived(Array.from(new Map(rawResults.map((item) => [item.id, item])).values()))
// Infinite scroll with IsInViewport
let sentinelEl = $state<HTMLElement>()
const inViewport = new IsInViewport(() => sentinelEl, { rootMargin: '200px' })
$effect(() => {
if (inViewport.current && query.hasNextPage && !query.isFetchingNextPage && !query.isLoading) {
query.fetchNextPage()
}
})
Debounced Search Pattern
let searchQuery = $state('')
let debouncedSearchQuery = $state('')
let debounceTimer: ReturnType<typeof setTimeout> | undefined
$effect(() => {
const query = searchQuery
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
debouncedSearchQuery = query
}, 300)
return () => { if (debounceTimer) clearTimeout(debounceTimer) }
})
// Use debouncedSearchQuery in the query, not searchQuery
const query = createInfiniteQuery(() => queryOptions.search(debouncedSearchQuery))
Type Assertions for Conditional Queries
When a query can return different types based on conditions, use type assertions:
const query = createInfiniteQuery(() => {
if (condition) {
return queryOptionsA()
}
return queryOptionsB() as unknown as ReturnType<typeof queryOptionsA>
})
Mutation Pattern
import { useUpdateParty } from '$lib/api/mutations/party.mutations'
const updatePartyMutation = useUpdateParty()
function handleSave() {
updatePartyMutation.mutate(
{ partyId, updates },
{
onSuccess: () => { /* handle success */ },
onError: (error) => { /* handle error */ }
}
)
}
// Use mutation state for UI
{#if updatePartyMutation.isPending}
<span>Saving...</span>
{/if}
Known Issues
Pre-existing Build Errors
The build has pre-existing errors unrelated to TanStack Query migration:
Cannot find module '$lib/paraglide/server'inhooks.server.tsCannot find module '$lib/paraglide/runtime'inhooks.tsCannot find module '$lib/paraglide/messages'in various components
These are paraglide i18n setup issues and should be ignored when checking for migration-related errors.
Duplicate Key Error Fix
When using infinite queries, the API may return duplicate items across pages. Always deduplicate:
const rawResults = $derived(query.data?.pages.flatMap((page) => page.results) ?? [])
const items = $derived(Array.from(new Map(rawResults.map((item) => [item.id, item])).values()))
File Locations
Query Options Factories
src/lib/api/queries/party.queries.tssrc/lib/api/queries/job.queries.tssrc/lib/api/queries/user.queries.tssrc/lib/api/queries/search.queries.ts
Mutation Hooks
src/lib/api/mutations/party.mutations.tssrc/lib/api/mutations/grid.mutations.tssrc/lib/api/mutations/job.mutations.ts
SSR Utilities
src/lib/query/ssr.ts
Reference Implementations
src/lib/components/sidebar/JobSelectionSidebar.svelte- Simple infinite querysrc/lib/components/sidebar/JobSkillSelectionSidebar.svelte- Infinite query with searchsrc/lib/components/sidebar/SearchContent.svelte- Infinite query with filters and deduplicationsrc/routes/teams/[id]/+page.svelte- SSR with initialDatasrc/routes/[username]/+page.svelte- Conditional queries (teams vs favorites)src/routes/teams/explore/+page.svelte- Simple infinite scroll page
Commands
# Run TypeScript check
pnpm run check
# Run development server
pnpm run dev
# Check for resource class imports
grep -r "from.*resources/" src/
# Check for createInfiniteScrollResource usage
grep -r "createInfiniteScrollResource" src/
Branch Information
- Base branch:
svelte-main - PR #442 branch:
devin/1764405731-tanstack-query-migration-phase2