/** * Performance-optimized invitation hooks. * Uses useSyncExternalStore for fine-grained reactivity. */ import { useSyncExternalStore, useMemo, useCallback } from 'react'; import type { Invitation } from '../../services/invitation.js'; import type { XOInvitation } from '@xo-cash/types'; import { useAppContext } from './useAppContext.js'; /** * Get all invitations reactively. * Re-renders when invitations are added or removed. */ export function useInvitations(): Invitation[] { const { appService } = useAppContext(); const subscribe = useCallback( (callback: () => void) => { if (!appService) { return () => {}; } // Subscribe to invitation list changes const onAdded = () => callback(); const onRemoved = () => callback(); appService.on('invitation-added', onAdded); appService.on('invitation-removed', onRemoved); return () => { appService.off('invitation-added', onAdded); appService.off('invitation-removed', onRemoved); }; }, [appService] ); const getSnapshot = useCallback(() => { return appService?.invitations ?? []; }, [appService]); return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); } /** * Get a single invitation by ID with selective re-rendering. * Only re-renders when the specific invitation is updated. */ export function useInvitation(invitationId: string | null): Invitation | null { const { appService } = useAppContext(); const subscribe = useCallback( (callback: () => void) => { if (!appService || !invitationId) { return () => {}; } // Find the invitation instance const invitation = appService.invitations.find( (inv) => inv.data.invitationIdentifier === invitationId ); if (!invitation) { return () => {}; } // Subscribe to this specific invitation's updates const onUpdated = () => callback(); const onStatusChanged = () => callback(); invitation.on('invitation-updated', onUpdated); invitation.on('invitation-status-changed', onStatusChanged); // Also subscribe to list changes in case the invitation is removed const onRemoved = () => callback(); appService.on('invitation-removed', onRemoved); return () => { invitation.off('invitation-updated', onUpdated); invitation.off('invitation-status-changed', onStatusChanged); appService.off('invitation-removed', onRemoved); }; }, [appService, invitationId] ); const getSnapshot = useCallback(() => { if (!appService || !invitationId) { return null; } return ( appService.invitations.find( (inv) => inv.data.invitationIdentifier === invitationId ) ?? null ); }, [appService, invitationId]); return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); } /** * Get invitation data with memoization. * Returns stable references to prevent unnecessary re-renders. */ export function useInvitationData(invitationId: string | null): XOInvitation | null { const invitation = useInvitation(invitationId); return useMemo(() => { return invitation?.data ?? null; }, [invitation?.data.invitationIdentifier, invitation?.data.commits?.length]); } /** * Hook to create invitations. * Returns a memoized function to create invitations. */ export function useCreateInvitation() { const { appService } = useAppContext(); return useCallback( async (invitation: XOInvitation | string): Promise => { if (!appService) { throw new Error('AppService not initialized'); } return await appService.createInvitation(invitation); }, [appService] ); } /** * Hook to get all invitations with their IDs. * Useful for lists where you only need IDs (prevents re-renders on data changes). */ export function useInvitationIds(): string[] { const invitations = useInvitations(); return useMemo(() => { return invitations.map((inv) => inv.data.invitationIdentifier); }, [invitations]); }