Zustand selectors returning new Set() on every call fail Object.is equality check, triggering continuous re-renders. Replaced with useMemo-derived Sets in consuming components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
105 lines
2.8 KiB
TypeScript
105 lines
2.8 KiB
TypeScript
import { create } from 'zustand'
|
|
import { pinnedFlowsApi } from '@/api/pinnedFlows'
|
|
import type { PinnedFlow } from '@/api/pinnedFlows'
|
|
import { toast } from '@/lib/toast'
|
|
|
|
interface PinnedFlowsState {
|
|
items: PinnedFlow[]
|
|
isLoaded: boolean
|
|
isLoading: boolean
|
|
isMutatingByTreeId: Record<string, boolean>
|
|
error: string | null
|
|
|
|
load: (force?: boolean) => Promise<void>
|
|
pin: (treeId: string) => Promise<void>
|
|
unpin: (treeId: string) => Promise<void>
|
|
toggle: (treeId: string) => void
|
|
isPinned: (treeId: string) => boolean
|
|
}
|
|
|
|
export const usePinnedFlowsStore = create<PinnedFlowsState>()((set, get) => ({
|
|
items: [],
|
|
isLoaded: false,
|
|
isLoading: false,
|
|
isMutatingByTreeId: {},
|
|
error: null,
|
|
|
|
load: async (force = false) => {
|
|
const state = get()
|
|
if (state.isLoaded && !force) return
|
|
if (state.isLoading) return
|
|
|
|
set({ isLoading: true, error: null })
|
|
try {
|
|
const data = await pinnedFlowsApi.list()
|
|
set({ items: data.items, isLoaded: true, isLoading: false })
|
|
} catch {
|
|
set({ error: 'Failed to load pinned flows', isLoading: false })
|
|
}
|
|
},
|
|
|
|
pin: async (treeId: string) => {
|
|
const state = get()
|
|
if (state.isMutatingByTreeId[treeId]) return
|
|
|
|
set({ isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true } })
|
|
|
|
try {
|
|
await pinnedFlowsApi.pin(treeId)
|
|
const data = await pinnedFlowsApi.list()
|
|
set((s) => ({
|
|
items: data.items,
|
|
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
|
|
}))
|
|
} catch (err: unknown) {
|
|
const status = (err as { response?: { status?: number } })?.response?.status
|
|
if (status === 409) {
|
|
toast.error('Maximum of 15 favorites reached. Unpin a flow to add a new one.')
|
|
} else {
|
|
toast.error('Failed to pin flow')
|
|
}
|
|
set((s) => ({
|
|
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
|
|
}))
|
|
}
|
|
},
|
|
|
|
unpin: async (treeId: string) => {
|
|
const state = get()
|
|
if (state.isMutatingByTreeId[treeId]) return
|
|
|
|
const prevItems = state.items
|
|
set({
|
|
items: state.items.filter((f) => f.tree_id !== treeId),
|
|
isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true },
|
|
})
|
|
|
|
try {
|
|
await pinnedFlowsApi.unpin(treeId)
|
|
set((s) => ({
|
|
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
|
|
}))
|
|
} catch {
|
|
toast.error('Failed to unpin flow')
|
|
set((s) => ({
|
|
items: prevItems,
|
|
isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
|
|
}))
|
|
}
|
|
},
|
|
|
|
toggle: (treeId: string) => {
|
|
const state = get()
|
|
if (state.isPinned(treeId)) {
|
|
state.unpin(treeId)
|
|
} else {
|
|
state.pin(treeId)
|
|
}
|
|
},
|
|
|
|
isPinned: (treeId: string) => {
|
|
return get().items.some((f) => f.tree_id === treeId)
|
|
},
|
|
}))
|
|
|