From 8f10dd2873ab365c37946f47a3061b3c208b2701 Mon Sep 17 00:00:00 2001 From: Mikhail Golbakh Date: Wed, 10 Jul 2024 00:01:15 +0300 Subject: [PATCH] feat: add observer and useData --- src/core/index.ts | 2 + src/core/types/DataSource.ts | 18 ++++++ src/react-query/hooks/useQueryData.ts | 20 ++++++- src/react-query/impl/infinite/factory.ts | 4 ++ src/react-query/impl/infinite/observer.ts | 69 +++++++++++++++++++++++ src/react-query/impl/plain/factory.ts | 4 ++ src/react-query/impl/plain/observer.ts | 69 +++++++++++++++++++++++ src/react/index.ts | 2 + src/react/useData.ts | 37 ++++++++++++ 9 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 src/react-query/impl/infinite/observer.ts create mode 100644 src/react-query/impl/plain/observer.ts create mode 100644 src/react/useData.ts diff --git a/src/core/index.ts b/src/core/index.ts index ec39a57..c3e6678 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -3,6 +3,8 @@ export type { DataSourceTag, DataSource, AnyDataSource, + DataObserver, + DataListener, DataSourceContext, DataSourceParams, DataSourceRequest, diff --git a/src/core/types/DataSource.ts b/src/core/types/DataSource.ts index 51383e2..910b6ad 100644 --- a/src/core/types/DataSource.ts +++ b/src/core/types/DataSource.ts @@ -24,6 +24,11 @@ export interface DataSource< fetchContext: TFetchContext, request: TRequest, ) => Promise | TResponse; + observe: ( + context: TContext, + params: ActualParams, + options?: TOptions, + ) => DataObserver; tags?: (params: ActualParams) => DataSourceTag[]; transformParams?: (params: TParams) => TRequest; @@ -38,6 +43,19 @@ export interface DataSource< // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AnyDataSource = DataSource; +export interface DataObserver { + getCurrentState(): DataSourceState; + updateParams( + params: DataSourceParams, + options?: DataSourceOptions, + ): void; + subscribe(listener: DataListener): () => void; +} + +export type DataListener = ( + state: DataSourceState, +) => void; + export type DataSourceContext = TDataSource extends DataSource< infer TContext, diff --git a/src/react-query/hooks/useQueryData.ts b/src/react-query/hooks/useQueryData.ts index f6dd069..f130b62 100644 --- a/src/react-query/hooks/useQueryData.ts +++ b/src/react-query/hooks/useQueryData.ts @@ -1,4 +1,10 @@ -import type {DataSourceOptions, DataSourceParams, DataSourceState} from '../../core'; +import type { + DataSourceContext, + DataSourceOptions, + DataSourceParams, + DataSourceState, +} from '../../core'; +import {useData} from '../../react'; import {useInfiniteQueryData} from '../impl/infinite/hooks'; import {usePlainQueryData} from '../impl/plain/hooks'; import type {AnyQueryDataSource} from '../types'; @@ -27,3 +33,15 @@ export const useQueryData = ( return state as DataSourceState; }; + +// Do not use it yet. It will be reworked +export const _useQueryData = ( + dataSource: TDataSource, + params: DataSourceParams, + options?: DataSourceOptions, +) => { + const context = useQueryContext() as DataSourceContext; + const [state] = useData(dataSource, context, params, options); + + return state; +}; diff --git a/src/react-query/impl/infinite/factory.ts b/src/react-query/impl/infinite/factory.ts index 7a2cec9..0b49ff1 100644 --- a/src/react-query/impl/infinite/factory.ts +++ b/src/react-query/impl/infinite/factory.ts @@ -1,3 +1,4 @@ +import {InfiniteQueryDataObserver} from './observer'; import type {InfiniteQueryDataSource} from './types'; export const makeInfiniteQueryDataSource = ( @@ -5,4 +6,7 @@ export const makeInfiniteQueryDataSource = => ({ ...config, type: 'infinite', + observe(context, params, options) { + return new InfiniteQueryDataObserver(context, this, params, options); + }, }); diff --git a/src/react-query/impl/infinite/observer.ts b/src/react-query/impl/infinite/observer.ts new file mode 100644 index 0000000..4345c64 --- /dev/null +++ b/src/react-query/impl/infinite/observer.ts @@ -0,0 +1,69 @@ +import type {InfiniteQueryObserverResult} from '@tanstack/react-query'; +import {InfiniteQueryObserver} from '@tanstack/react-query'; + +import type { + DataListener, + DataObserver, + DataSourceContext, + DataSourceData, + DataSourceError, + DataSourceOptions, + DataSourceParams, + DataSourceResponse, +} from '../../../core'; + +import type {AnyInfiniteQueryDataSource} from './types'; +import {composeOptions, transformResult} from './utils'; + +export class InfiniteQueryDataObserver< + TDataSource extends AnyInfiniteQueryDataSource, + TContext extends DataSourceContext = DataSourceContext, + TParams extends DataSourceParams = DataSourceParams, + TResponse extends DataSourceResponse = DataSourceResponse, + TData extends DataSourceData = DataSourceData, + TError extends DataSourceError = DataSourceError, + TOptions extends DataSourceOptions = DataSourceOptions, +> implements DataObserver +{ + readonly context: TContext; + readonly dataSource: TDataSource; + readonly observer: InfiniteQueryObserver; + + constructor(context: TContext, dataSource: TDataSource, params: TParams, options?: TOptions) { + this.context = context; + this.dataSource = dataSource; + this.observer = new InfiniteQueryObserver( + context.queryClient, + this.composeOptions(context, dataSource, params, options), + ); + } + + getCurrentState() { + return this.transformResult(this.observer.getCurrentResult()); + } + + updateParams(params: TParams, options?: TOptions) { + this.observer.setOptions( + this.composeOptions(this.context, this.dataSource, params, options), + ); + } + + subscribe(listener: DataListener) { + return this.observer.subscribe((result) => { + listener(this.transformResult(result)); + }); + } + + private composeOptions( + context: TContext, + dataSource: TDataSource, + params: TParams, + options?: TOptions, + ) { + return composeOptions(context, dataSource, params, options); + } + + private transformResult(result: InfiniteQueryObserverResult) { + return transformResult(result); + } +} diff --git a/src/react-query/impl/plain/factory.ts b/src/react-query/impl/plain/factory.ts index 44254a6..aa29095 100644 --- a/src/react-query/impl/plain/factory.ts +++ b/src/react-query/impl/plain/factory.ts @@ -1,3 +1,4 @@ +import {PlainQueryDataObserver} from './observer'; import type {PlainQueryDataSource} from './types'; export const makePlainQueryDataSource = ( @@ -5,4 +6,7 @@ export const makePlainQueryDataSource = => ({ ...config, type: 'plain', + observe(context, params, options) { + return new PlainQueryDataObserver(context, this, params, options); + }, }); diff --git a/src/react-query/impl/plain/observer.ts b/src/react-query/impl/plain/observer.ts new file mode 100644 index 0000000..4ebabb5 --- /dev/null +++ b/src/react-query/impl/plain/observer.ts @@ -0,0 +1,69 @@ +import type {QueryObserverOptions, QueryObserverResult} from '@tanstack/react-query'; +import {QueryObserver} from '@tanstack/react-query'; + +import type { + DataListener, + DataObserver, + DataSourceContext, + DataSourceData, + DataSourceError, + DataSourceOptions, + DataSourceParams, + DataSourceResponse, +} from '../../../core'; + +import type {AnyPlainQueryDataSource} from './types'; +import {composeOptions, transformResult} from './utils'; + +export class PlainQueryDataObserver< + TDataSource extends AnyPlainQueryDataSource, + TContext extends DataSourceContext = DataSourceContext, + TParams extends DataSourceParams = DataSourceParams, + TResponse extends DataSourceResponse = DataSourceResponse, + TData extends DataSourceData = DataSourceData, + TError extends DataSourceError = DataSourceError, + TOptions extends DataSourceOptions = DataSourceOptions, +> implements DataObserver +{ + readonly context: TContext; + readonly dataSource: TDataSource; + readonly observer: QueryObserver; + + constructor(context: TContext, dataSource: TDataSource, params: TParams, options?: TOptions) { + this.context = context; + this.dataSource = dataSource; + this.observer = new QueryObserver( + context.queryClient, + this.composeOptions(context, dataSource, params, options), + ); + } + + getCurrentState() { + return this.transformResult(this.observer.getCurrentResult()); + } + + updateParams(params: TParams, options?: TOptions) { + this.observer.setOptions( + this.composeOptions(this.context, this.dataSource, params, options), + ); + } + + subscribe(listener: DataListener) { + return this.observer.subscribe((result) => { + listener(this.transformResult(result)); + }); + } + + private composeOptions( + context: TContext, + dataSource: TDataSource, + params: TParams, + options?: TOptions, + ): QueryObserverOptions { + return composeOptions(context, dataSource, params, options); + } + + private transformResult(result: QueryObserverResult) { + return transformResult(result); + } +} diff --git a/src/react/index.ts b/src/react/index.ts index 8710d76..f63178f 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -1,3 +1,5 @@ +export {useData} from './useData'; + export {DataManagerContext, useDataManager} from './DataManagerContext'; export type {WithDataManagerProps} from './withDataManager'; diff --git a/src/react/useData.ts b/src/react/useData.ts new file mode 100644 index 0000000..7b19360 --- /dev/null +++ b/src/react/useData.ts @@ -0,0 +1,37 @@ +import {useEffect, useState} from 'react'; + +import type { + AnyDataSource, + DataObserver, + DataSourceContext, + DataSourceOptions, + DataSourceParams, + DataSourceState, +} from '../core'; +import {composeKey} from '../core'; + +export const useData = ( + dataSource: TDataSource, + context: DataSourceContext, + params: DataSourceParams, + options?: DataSourceOptions, +): [DataSourceState, DataObserver] => { + const [observer] = useState(() => dataSource.observe(context, params, options)); + const [state, setState] = useState>(() => + observer.getCurrentState(), + ); + + useEffect(() => { + return observer.subscribe(setState); + }, [observer]); + + const key = composeKey(dataSource, params); + + useEffect(() => { + observer.updateParams(params, options); + // Key replaces params and other deps are static + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [options, key]); + + return [state, observer]; +};