vega-frontend-monorepo/libs/data-provider/src/use-data-provider.ts

219 lines
5.7 KiB
TypeScript

import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import throttle from 'lodash/throttle';
import isEqualWith from 'lodash/isEqualWith';
import { useApolloClient } from '@apollo/client';
import type { OperationVariables } from '@apollo/client';
import type { Subscribe, Load, UpdateCallback } from './generic-data-provider';
import { variablesIsEqualCustomizer } from './generic-data-provider';
export interface useDataProviderParams<
Data,
Delta,
Variables extends OperationVariables | undefined = undefined
> {
dataProvider: Subscribe<Data, Delta, Variables>;
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
insert?: ({
insertionData,
data,
}: {
insertionData?: Data | null;
data: Data | null;
}) => boolean;
variables: Variables;
skipUpdates?: boolean;
skip?: boolean;
}
/**
*
* @param dataProvider subscribe function created by makeDataProvider
* @param update optional function called on each delta received in subscription, if returns true updated data will be not passed from hook (component handles updates internally)
* @param variables optional
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
*/
export const useDataProvider = <
Data,
Delta,
Variables extends OperationVariables | undefined = undefined
>({
dataProvider,
update,
insert,
skipUpdates,
skip,
...props
}: useDataProviderParams<Data, Delta, Variables>) => {
const client = useApolloClient();
const [data, setData] = useState<Data | null>(null);
const [loading, setLoading] = useState<boolean>(!skip);
const [error, setError] = useState<Error | undefined>(undefined);
const flushRef = useRef<(() => void) | undefined>(undefined);
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
const loadRef = useRef<Load<Data> | undefined>(undefined);
const variablesRef = useRef<Variables>(props.variables);
const updateRef = useRef(update);
const insertRef = useRef(insert);
const skipUpdatesRef = useRef(skipUpdates);
const variables = useMemo(() => {
if (
!isEqualWith(
variablesRef.current,
props.variables,
variablesIsEqualCustomizer
)
) {
variablesRef.current = props.variables;
}
return variablesRef.current;
}, [props.variables]);
const flush = useCallback(() => {
if (flushRef.current) {
flushRef.current();
}
}, []);
const reload = useCallback((force = false) => {
if (reloadRef.current) {
reloadRef.current(force);
}
}, []);
const load = useCallback<Load<Data>>((...args) => {
if (loadRef.current) {
return loadRef.current(...args);
}
return Promise.reject();
}, []);
const callback = useCallback<UpdateCallback<Data, Delta>>((args) => {
const {
data,
delta,
error,
loading,
insertionData,
isInsert,
isUpdate,
loaded,
} = args;
setError(error);
setLoading(!loaded && loading);
// if update or insert function returns true it means that component handles updates
// component can use flush() which will call callback without delta and cause data state update
if (!loading) {
if (
isUpdate &&
(skipUpdatesRef.current ||
(!skipUpdatesRef.current &&
updateRef.current &&
updateRef.current({ delta, data })))
) {
return;
}
if (
isInsert &&
insertRef.current &&
insertRef.current({ insertionData, data })
) {
return;
}
}
setData(data);
if (!loading && !isUpdate && updateRef.current) {
updateRef.current({ data });
}
}, []);
useEffect(() => {
updateRef.current = update;
}, [update]);
useEffect(() => {
insertRef.current = insert;
}, [insert]);
useEffect(() => {
skipUpdatesRef.current = skipUpdates;
}, [skipUpdates]);
useEffect(() => {
setData(null);
setError(undefined);
if (updateRef.current) {
updateRef.current({ data: null });
}
if (skip) {
setLoading(false);
if (updateRef.current) {
updateRef.current({ data: null });
}
return;
}
setLoading(true);
const { unsubscribe, flush, reload, load } = dataProvider(
callback,
client,
variables
);
flushRef.current = flush;
reloadRef.current = reload;
loadRef.current = load;
return () => {
flushRef.current = undefined;
reloadRef.current = undefined;
loadRef.current = undefined;
return unsubscribe();
};
}, [client, dataProvider, callback, variables, skip]);
return {
data,
loading,
error,
flush,
reload,
load,
};
};
export const useThrottledDataProvider = <
Data,
Delta,
Variables extends OperationVariables = OperationVariables
>(
params: Omit<useDataProviderParams<Data, Delta, Variables>, 'update'>,
wait = 500
) => {
const [data, setData] = useState<Data | null>(null);
const dataRef = useRef<Data | null>(null);
const updateData = useRef(
throttle(() => {
if (!dataRef.current) {
return;
}
setData(dataRef.current);
}, wait)
);
const update = useCallback(({ data }: { data: Data | null }) => {
if (!data) {
return false;
}
dataRef.current = data;
updateData.current();
return true;
}, []);
const returnValues = useDataProvider({ ...params, update });
useEffect(() => {
const throttledUpdate = updateData.current;
return () => {
throttledUpdate.cancel();
};
}, []);
useEffect(() => {
setData(returnValues.data);
}, [returnValues.data]);
return { ...returnValues, data };
};