219 lines
5.7 KiB
TypeScript
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 };
|
|
};
|