Files
llink/js/desktop/src/hooks/use-particle.ts
T
Arjun PatelandGitHub a8a0b7db1b infra: add linting and formatting for js projects (#230)
* wip

* wip

* wip

* format

* wip(mobile): lint and format

* cleanup

* nits

* nits

* idiomatic react

* nit

* format all root files
2026-06-02 07:44:24 -07:00

202 lines
5.0 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
import {
subscribeToParticle,
subscribeToParticleChildren,
subscribeToLatestChild,
getParticle,
getParticleChildren,
} from '@/lib/firestore-particles';
import type { Particle } from '@/api/types';
import {
type ParticlePath,
toFirestoreDocPath,
toFirestoreChildrenPath,
} from '@/lib/particle-path';
import { useQuery } from '@tanstack/react-query';
import { QueryFieldFilterConstraint } from 'firebase/firestore';
interface UseLiveParticleResult {
particle: Particle | null;
isLoading: boolean;
error: Error | null;
}
export function useLiveParticle(path: ParticlePath): UseLiveParticleResult {
const [particle, setParticle] = useState<Particle | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const docPath = toFirestoreDocPath(path);
const unsubscribe = subscribeToParticle(
docPath,
(data) => {
setParticle(data);
setIsLoading(false);
},
(err) => {
setError(err);
setIsLoading(false);
},
);
return () => {
unsubscribe();
setIsLoading(true);
setError(null);
setParticle(null);
};
}, [path]);
return { particle, isLoading, error };
}
interface UseLiveParticleChildrenResult {
children: Particle[];
isLoading: boolean;
error: Error | null;
}
interface UseLiveParticleChildrenParams {
orderByField?: string;
orderDirection?: 'asc' | 'desc';
visibilityScopes?: string[];
onAdded?: (child: Particle) => void;
onRemoved?: (child: Particle, updatedChildren: Particle[]) => void;
whereFilter?: QueryFieldFilterConstraint;
/** Optional cap on results. Changes trigger a re-subscription. */
limit?: number;
}
export function useLiveParticleChildren(
path: ParticlePath | undefined,
{
orderByField = 'created_at',
orderDirection = 'desc',
visibilityScopes,
onAdded,
onRemoved,
whereFilter,
limit,
}: UseLiveParticleChildrenParams = {},
): UseLiveParticleChildrenResult {
const [children, setChildren] = useState<Particle[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
// Keep the latest add/remove callbacks in refs so changing them doesn't force
// the subscription to re-attach — they're notifications, not query params.
const onAddedRef = useRef(onAdded);
const onRemovedRef = useRef(onRemoved);
useEffect(() => {
onAddedRef.current = onAdded;
onRemovedRef.current = onRemoved;
}, [onAdded, onRemoved]);
useEffect(() => {
if (!path) return;
const collectionPath = toFirestoreChildrenPath(path);
const unsubscribe = subscribeToParticleChildren(collectionPath, {
onData: (data) => {
setChildren(data);
setIsLoading(false);
},
onError: (err) => {
console.warn(err);
setError(err);
setIsLoading(false);
},
visibilityScopes,
orderByField,
orderDirection,
onAdded: (child: Particle) => onAddedRef.current?.(child),
onRemoved: (child: Particle, updatedChildren: Particle[]) =>
onRemovedRef.current?.(child, updatedChildren),
whereFilter,
limit,
});
return () => {
unsubscribe();
setChildren([]);
setError(null);
setIsLoading(true);
};
}, [
path,
whereFilter,
limit,
orderByField,
orderDirection,
visibilityScopes,
]);
// No path: nothing to load, so report an empty non-loading state.
if (!path) {
return { children: [], isLoading: false, error: null };
}
return { children, isLoading, error };
}
interface UseLiveLatestChildResult {
latestChild: Particle | null;
isLoading: boolean;
}
export function useLiveLatestChild(
path: ParticlePath,
): UseLiveLatestChildResult {
const [latestChild, setLatestChild] = useState<Particle | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const unsubscribe = subscribeToLatestChild(
toFirestoreChildrenPath(path),
(data) => {
setLatestChild(data);
setIsLoading(false);
},
() => {
setIsLoading(false);
},
);
return () => {
unsubscribe();
setIsLoading(true);
setLatestChild(null);
};
}, [path]);
return { latestChild, isLoading };
}
export function useParticle(path?: ParticlePath) {
return useQuery({
queryKey: ['particle', path],
queryFn: async () => {
if (!path) return null;
const docPath = toFirestoreDocPath(path);
const particle = await getParticle(docPath);
return particle;
},
enabled: !!path,
});
}
export function useParticleChildren(path?: ParticlePath) {
return useQuery({
queryKey: ['particle-children', path],
queryFn: async () => {
if (!path) return [];
const collectionPath = toFirestoreChildrenPath(path);
return getParticleChildren(collectionPath);
},
enabled: !!path,
staleTime: 1000 * 60 * 5, // 5 min — attachments don't change
});
}