UNPKG

synapse-storage

Version:

Набор инструментов для управления состоянием и апи-запросами

121 lines (116 loc) 5.42 kB
import { useCallback, useEffect, useRef, useState } from "react"; /** * Стабильная сериализация параметров для ключа зависимостей эффекта. * Сортирует ключи объектов рекурсивно, поэтому `{ a: 1, b: 2 }` и `{ b: 2, a: 1 }` * дают одинаковую строку — новый объект-литерал на каждый рендер не вызывает * бесконечный ре-запрос. */ function stableStringify(value) { if (value === undefined) return 'undefined'; if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'; if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`; const obj = value; const keys = Object.keys(obj).sort(); return `{${keys.map((k)=>`${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(',')}}`; } const IDLE_STATE = { status: 'idle', data: undefined, error: undefined, fromCache: false }; /** Синхронно читает кэш эндпоинта (если доступно) и приводит к состоянию запроса. */ function readSyncCache(endpoint, params) { const cached = endpoint.getCachedSync(params); if (cached?.ok) { return { status: 'success', data: cached.data, error: undefined, fromCache: true }; } return undefined; } /** * Хук для выполнения GET-запросов через эндпоинт ApiClient в стиле React Query. * * Тонкая обёртка над `endpoint.request(params).subscribe(...)`: дедупликация, * кэш по тегам, отмена и retry уже реализованы в самом эндпоинте. Хук добавляет * React-слой: подписку, стабильный ключ параметров, `enabled`/`refetch`, * SSR-fast-path (синхронное чтение кэша на первом рендере без вспышки loading) * и авто-рефетч при инвалидации кэша. * * @example * const { data, isLoading, error, refetch } = useApiQuery(endpoints.getUser, { id }) */ function useApiQuery(endpoint, params, options = {}) { const { enabled = true, refetchOnInvalidate = true, ...queryOptions } = options; const paramsKey = stableStringify(params); // refs — чтобы effect зависел только от стабильного ключа, а не от идентичности объектов const paramsRef = useRef(params); paramsRef.current = params; const optionsRef = useRef(queryOptions); optionsRef.current = queryOptions; // Ленивый инициализатор важен для SSR: на сервере effect не выполняется, поэтому // первый (и единственный) рендер должен сразу отдать засеянные/кэшированные данные. const [state, setState] = useState(()=>enabled ? readSyncCache(endpoint, params) ?? IDLE_STATE : IDLE_STATE); const [refetchToken, setRefetchToken] = useState(0); const refetch = useCallback(()=>setRefetchToken((t)=>t + 1), []); useEffect(()=>{ if (!enabled) { setState(IDLE_STATE); return; } // На смену параметров: показываем кэш сразу (без вспышки) либо loading. setState(readSyncCache(endpoint, paramsRef.current) ?? { status: 'loading', data: undefined, error: undefined, fromCache: false }); let cancelled = false; const req = endpoint.request(paramsRef.current, optionsRef.current); const unsubscribe = req.subscribe((s)=>{ // 'idle' — начальный эхо-снапшот подписки; игнорируем, чтобы не сбрасывать // уже показанные данные. Реальные переходы — loading/success/error. if (cancelled || s.status === 'idle') return; setState({ status: s.status, data: s.data, error: s.error, fromCache: s.fromCache }); }, { autoUnsubscribe: false }); return ()=>{ cancelled = true; req.abort(); unsubscribe(); }; }, [ endpoint, paramsKey, enabled, refetchToken ]); // Авто-рефетч при инвалидации кэша по тегам этого эндпоинта (например, после мутации) useEffect(()=>{ if (!enabled || !refetchOnInvalidate) return undefined; return endpoint.onCacheInvalidate(()=>setRefetchToken((t)=>t + 1)); }, [ endpoint, enabled, refetchOnInvalidate ]); return { data: state.data, error: state.error, status: state.status, isLoading: state.status === 'loading', isError: state.status === 'error', isSuccess: state.status === 'success', fromCache: state.fromCache, refetch }; } export { useApiQuery }; //# sourceMappingURL=useApiQuery.js.map