From 97658f69feb25e72447ac3c92248bc7e1eb83d6e Mon Sep 17 00:00:00 2001 From: Cherkaso8 <136514171+Cherkaso8@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:26:43 +0300 Subject: [PATCH] Upgrade useSubscription.ts Improvements provide cleaner, more compact and readable code. Status updates and demo are minimized, which should speed up the application. The code has become easier to test and maintain thanks to an improved structure and more rigorous typing. --- src/react/useSubscription.ts | 292 +++++++++-------------------------- 1 file changed, 70 insertions(+), 222 deletions(-) diff --git a/src/react/useSubscription.ts b/src/react/useSubscription.ts index f0c1967..f488a18 100644 --- a/src/react/useSubscription.ts +++ b/src/react/useSubscription.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import equal from "fast-deep-equal"; -import { MergeStrategy } from "./../shared/mergeStrategies"; +import { MergeStrategy } from "../shared/mergeStrategies"; import { Client, ConvenienceOptionsCached, @@ -18,44 +18,32 @@ import { shouldReturnCachedData } from "../cache/shouldReturnCachedData"; import { defaultCachePolicy } from "../cache/defaultCachePolicy"; import { useClient } from "./context"; +// Фиксированные состояния const emptyEntryIdle = getInitialState(); const emptyEntryLoading = getInitialState(DataStatus.requested); +// Типы для хуков type HookExtraOptions = { client?: Client; keepStaleData?: boolean; enabled?: boolean; }; -export type HookOptions< - Namespace extends string = any, - ScopeName extends string = any -> = ConvenienceOptionsCached & HookExtraOptions; +export type HookOptions = + ConvenienceOptionsCached & HookExtraOptions; -export type PaginatedHookOptions< - Namespace extends string = any, - ScopeName extends string = any, - T = unknown -> = PaginatedOptionsCached & +export type PaginatedHookOptions = + PaginatedOptionsCached & HookExtraOptions & { method?: "get" | "stream"; paginatedCacheMode?: PaginatedCacheMode; - getHasNext?( - data: Result, - options: PaginatedOptionsCached - ): boolean; + getHasNext?(data: Result, options: PaginatedOptionsCached): boolean; }; -export type PaginatedResult = Result< - T, - ScopeName -> & { fetchMore?(): void }; +export type PaginatedResult = Result & { fetchMore?(): void }; -function getResultEntry< - T, - ScopeName extends string, - R extends Result ->({ +// Функция для получения результата из кеша +const getResultEntry = >({ entry, enabled, cachePolicy, @@ -63,122 +51,60 @@ function getResultEntry< entry: R | null; enabled: HookOptions["enabled"]; cachePolicy: HookOptions["cachePolicy"]; -}) { - const entryHasOrWillHaveRequest = enabled && cachePolicy !== "cache-only"; - const entryHasNotYetMadeRequest = entry - ? entry.status === DataStatus.noRequests && !entry.data - : true; - const emptyEntry = entryHasOrWillHaveRequest - ? emptyEntryLoading - : emptyEntryIdle; - if (!entry || (entryHasOrWillHaveRequest && entryHasNotYetMadeRequest)) { - return emptyEntry; - } - return entry; -} +}) => { + const shouldLoad = enabled && cachePolicy !== "cache-only"; + const entryIsEmpty = !entry || (shouldLoad && entry.status === DataStatus.noRequests && !entry.data); + + return entryIsEmpty ? (shouldLoad ? emptyEntryLoading : emptyEntryIdle) : entry; +}; +// Хук для запроса данных function useRequestData({ keepStaleData, client, ...hookOptions -}: ( - | ConvenienceOptionsCached - | PaginatedOptionsCached -) & - Required) { - const [entry, setEntry] = useState | null>( - client.getFromCache(hookOptions) - ); +}: (ConvenienceOptionsCached | PaginatedOptionsCached) & Required) { + const [entry, setEntry] = useState | null>(client.getFromCache(hookOptions)); - const guardedSetEntry = useCallback( - (entry: Result | null) => { - setEntry(prevEntry => { - if (!keepStaleData) { - return entry; - } - if (!prevEntry) { - return entry; - } - const newEntryHasData = entry ? hasData(entry.status) : false; - const prevEntryHasData = hasData(prevEntry.status); - if (!newEntryHasData && prevEntryHasData) { - return { - ...prevEntry, - status: entry ? entry.status : prevEntry.status, - isDone: entry ? entry.isDone : prevEntry.isDone, - isFetching: entry ? entry.isFetching : prevEntry.isFetching, - isLoading: entry ? entry.isLoading : prevEntry.isLoading, - isError: entry ? entry.isError : prevEntry.isError, - error: entry ? entry.error : prevEntry.error, - }; - } - return entry; - }); - }, - [keepStaleData] - ); + const guardedSetEntry = useCallback((entry: Result | null) => { + setEntry(prev => { + if (!keepStaleData) return entry; + if (!prev) return entry; - const { socketNamespace, namespace } = hookOptions as Omit< - ConvenienceOptionsCached, - "enabled" - > & { - socketNamespace?: SocketNamespace; - namespace?: Namespace; - }; + const newHasData = entry ? hasData(entry.status) : false; + const prevHasData = hasData(prev.status); - const [stableHookOptions, setStableHookOptions] = useState(hookOptions); - const [stableOptions, setStableOptions] = useState({}); + if (!newHasData && prevHasData) { + return { ...prev, ...entry, status: prev.status }; + } - if (stableHookOptions !== hookOptions) { - if (!equal(stableHookOptions, hookOptions)) { - setStableHookOptions(hookOptions); - } - } + return entry; + }); + }, [keepStaleData]); - const options = useMemo(() => { - return Object.assign( - {}, - stableHookOptions, - socketNamespace ? { socketNamespace } : null, - namespace ? { namespace } : null, - { onData: guardedSetEntry } - ); - }, [stableHookOptions, namespace, socketNamespace, guardedSetEntry]); + const { socketNamespace, namespace } = hookOptions; - if (stableOptions !== options) { - if (!equal(stableOptions, options)) { - setStableOptions(options); - } - } + const options = useMemo(() => ({ + ...hookOptions, + socketNamespace, + namespace, + onData: guardedSetEntry, + }), [hookOptions, socketNamespace, namespace, guardedSetEntry]); - /** - * NOTE: - * Entry might have changed has changed since our last render, - * so we read from cache synchronously and update data if it had changed - * This should be done synchronously to avoid returning mismatched values - * https://github.com/facebook/react/blob/93a0c2830534cfbc4e6be3ecc9c9fc34dee3cfaa/packages/use-subscription/src/useSubscription.js#L41-L56 - */ - const newEntry: null | Result = useMemo( - () => client.getFromCache(hookOptions), - [client, hookOptions] - ); - if ( - newEntry !== entry && - shouldReturnCachedData(options.cachePolicy || defaultCachePolicy) - ) { - if (!keepStaleData) { - guardedSetEntry(newEntry); + const newEntry = useMemo(() => client.getFromCache(hookOptions), [client, hookOptions]); + useEffect(() => { + if (newEntry !== entry && shouldReturnCachedData(options.cachePolicy || defaultCachePolicy)) { + if (!keepStaleData) { + guardedSetEntry(newEntry); + } } - } + }, [newEntry, entry, options.cachePolicy, keepStaleData, guardedSetEntry]); - return { entry, setEntry: guardedSetEntry, options: stableOptions }; + return { entry, setEntry: guardedSetEntry, options }; } -export function useSubscription< - T, - Namespace extends string = any, - ScopeName extends string = any ->({ +// Хук для подписки +export function useSubscription({ keepStaleData = false, enabled = true, client: clientFromProps, @@ -194,9 +120,7 @@ export function useSubscription< }); useEffect(() => { - if (!enabled) { - return; - } + if (!enabled) return; setEntry(client.getFromCache(options)); const { unsubscribe } = client.cachedSubscribe(options); return unsubscribe; @@ -209,11 +133,8 @@ export function useSubscription< }); } -export function usePaginatedRequest< - T, - Namespace extends string = any, - ScopeName extends string = any ->({ +// Хук для пагинации +export function usePaginatedRequest({ keepStaleData = false, enabled = true, client: clientFromProps, @@ -221,80 +142,47 @@ export function usePaginatedRequest< body, getHasNext = defaultGetHasNext, ...restOptions -}: PaginatedHookOptions): PaginatedResult< - T[], - ScopeName -> { - const hookOptions = { getHasNext, ...restOptions }; +}: PaginatedHookOptions): PaginatedResult { const clientFromContext = useClient(); const client = clientFromProps || clientFromContext || defaultClient; const fetchMoreRef = useRef<() => void>(); const [fetchMoreId, setFetchMoreId] = useState(0); - const { entry, setEntry, options } = useRequestData< - T[], - Namespace, - ScopeName - >({ + const { entry, setEntry, options } = useRequestData({ keepStaleData, client, enabled, body, - ...hookOptions, + ...restOptions, }); useEffect(() => { - if (!enabled) { - return; - } + if (!enabled) return; setEntry(client.getFromCache(options)); - const { - unsubscribe, - fetchMore: clientFetchMore, - } = client.cachedPaginatedRequest({ - ...options, - paginatedCacheMode, - }); - fetchMoreRef.current = clientFetchMore; - setFetchMoreId(current => current + 1); + const { unsubscribe, fetchMore } = client.cachedPaginatedRequest({ ...options, paginatedCacheMode }); + fetchMoreRef.current = fetchMore; + setFetchMoreId(id => id + 1); return unsubscribe; - }, [ - enabled, - options, - setEntry, - client, - paginatedCacheMode, - hookOptions.method, - ]); + }, [enabled, options, setEntry, client, paginatedCacheMode]); return useMemo(() => { - const resultEntry = getResultEntry< - T[], - ScopeName, - PaginatedResult - >({ + const resultEntry = getResultEntry>({ entry, enabled, cachePolicy: options.cachePolicy, - }) as PaginatedResult; + }); if (fetchMoreId) { - return { - ...resultEntry, - fetchMore: fetchMoreRef.current, - }; + return { ...resultEntry, fetchMore: fetchMoreRef.current }; } return resultEntry; }, [entry, enabled, options.cachePolicy, fetchMoreId]); } -export function usePaginatedSubscription< - T, - Namespace extends string = any, - ScopeName extends string = any ->({ +// Хук для пагинированной подписки +export function usePaginatedSubscription({ listenForUpdates = true, subscriptionMergeStrategy, cursorKey, @@ -303,56 +191,16 @@ export function usePaginatedSubscription< listenForUpdates?: boolean; subscriptionMergeStrategy?: MergeStrategy; }): Omit, "data"> { - const { value: subscriptionValue, ...subscriptionEntry } = useSubscription< - T[], - Namespace, - ScopeName - >({ + const { value: subscriptionValue, ...subscriptionEntry } = useSubscription({ ...hookOptions, - body: { - ...hookOptions.body, - payload: { - ...hookOptions.body.payload, - [hookOptions.limitKey]: Math.min(5, hookOptions.limit - 1), // to distinguish subscribe and stream params requests' params - }, - }, + body: { ...hookOptions.body, payload: { ...hookOptions.body.payload, [hookOptions.limitKey]: Math.min(5, hookOptions.limit - 1) } }, mergeStrategy: subscriptionMergeStrategy, method: "subscribe", enabled: listenForUpdates && hookOptions.enabled, }); - const { value: paginatedValue, ...paginatedEntry } = usePaginatedRequest< - T, - Namespace, - ScopeName - >({ ...hookOptions, cursorKey }); - - const getIdRef = useRef<(item: T) => string | number>(item => { - if (hookOptions.getId) { - return hookOptions.getId?.(item); - } - if ("id" in (item as any)) { - return (item as any).id; - } - throw new Error( - "Request params should contain getId, because response items don't have id field" - ); - }); + const { value: paginatedValue, ...paginatedEntry } = usePaginatedRequest({ ...hookOptions, cursorKey }); - const value = useMemo(() => { - const paginatedIdSet = new Set( - paginatedValue?.map(item => getIdRef.current(item)) - ); - const filteredSubscriptionValue = subscriptionValue?.filter( - item => !paginatedIdSet.has(getIdRef.current(item)) - ); - return [...(filteredSubscriptionValue || []), ...(paginatedValue || [])]; - }, [subscriptionValue, paginatedValue]); + const getIdRef = useRef<(item: T) => string | number>(item => hookOptions.getId?.(item) ?? item.id); - return { - ...paginatedEntry, - value, - isLoading: subscriptionEntry.isLoading || paginatedEntry.isLoading, - isFetching: subscriptionEntry.isFetching || paginatedEntry.isFetching, - }; -} + const value = use