UNPKG

@shopify/shop-minis-react

Version:

React component library for Shopify Shop Minis with Tailwind CSS v4 support (source-only, requires TypeScript)

194 lines (171 loc) 5.22 kB
import {useCallback, useEffect, useMemo, useState} from 'react' import { DataHookFetchPolicy, PaginatedDataHookReturnsBase, PaginationInfo, ShopActionResult, } from '../types' import {formatError, MiniError} from '../utils/errors' export interface ShopActionsDataFetchingResult<S> extends PaginatedDataHookReturnsBase { data: S | null } export const useShopActionsPaginatedDataFetching = < S = unknown, P extends {after?: string; fetchPolicy?: DataHookFetchPolicy} = { after?: undefined fetchPolicy?: DataHookFetchPolicy }, >( action: ( params: P ) => Promise<ShopActionResult<{data: S; pageInfo: PaginationInfo}>>, params: P, options: { skip?: boolean hook?: string validator?: (data: S) => void } ): ShopActionsDataFetchingResult<S> => { const [state, setState] = useState<{ data: S | null pageInfo: PaginationInfo loading: boolean error: Error | null }>({ data: null, pageInfo: {hasNextPage: false, endCursor: null}, loading: false, error: null, }) const skip = options?.skip === true const {validator, hook} = options const runValidator = useCallback( (dataToValidate: S): Error | null => { try { validator?.(dataToValidate) return null } catch (err) { if (err instanceof Error) return err return new MiniError({ hook, message: 'Validation failed', }) } }, [validator, hook] ) // Params object is recreated on every render, so we need to memoize it. // We don't know what's inside the params object, but we can stringify it. // eslint-disable-next-line react-hooks/exhaustive-deps const stableParams = useMemo(() => params, [JSON.stringify(params)]) // There's a lot of complexity here because each type of fetch has different side effects if we are trying to // stay close to how Apollo client works. eg: // - Initial fetch: set loading, set error, set data, reset on error (don't throw) // - change params fetch: set loading, set error, set data, reset on error (don't throw) // - refetch fetch: don't set loading, set error, update data, leave data as is was on error (also throw) // - fetchMore fetch: don't set loading, don't set error, update data, leave data as is was on error (also throw) const fetch = useCallback( async ( extraParams?: Partial<P>, { setLoading = true, setError = true, resetOnError = true, throwOnError = true, appendData = false, }: { setLoading?: boolean setError?: boolean resetOnError?: boolean throwOnError?: boolean appendData?: boolean } = {} ) => { let queryError: Error | null = null let validationError: Error | null = null setState(curState => ({ ...curState, loading: setLoading ? true : curState.loading, })) try { const result = await action({...stableParams, ...extraParams}) if (result.ok) { validationError = runValidator(result.data.data) setState(curState => { let newData = result.data.data if ( appendData && curState.data && Array.isArray(curState.data) && Array.isArray(result.data.data) ) { newData = [...curState.data, ...result.data.data] as S } return { ...curState, data: newData, pageInfo: result.data.pageInfo, loading: false, error: validationError ?? null, } }) } else { throw result.error } } catch (err) { console.log('caught 1', err) queryError = formatError({hook}, err) } const error = validationError || queryError if (error && (setError || resetOnError)) { setState(curState => ({ data: resetOnError ? null : curState.data, pageInfo: resetOnError ? {hasNextPage: false, endCursor: null} : curState.pageInfo, loading: false, error, })) } if (error && throwOnError) { throw error } }, [action, stableParams, hook, runValidator] ) const refetch = useCallback(async () => { await fetch({fetchPolicy: 'network-only'} as Partial<P>, { setLoading: false, resetOnError: false, throwOnError: true, }) }, [fetch]) const fetchMore = useCallback(async () => { if (!state.pageInfo.hasNextPage || !state.pageInfo.endCursor) return await fetch({after: state.pageInfo.endCursor} as Partial<P>, { setLoading: false, setError: false, resetOnError: false, throwOnError: true, appendData: true, }) }, [state.pageInfo.hasNextPage, state.pageInfo.endCursor, fetch]) useEffect(() => { if (skip) return fetch( {}, { throwOnError: false, } ) }, [fetch, skip]) return { data: state.data, loading: state.loading, error: state.error, hasNextPage: state.pageInfo.hasNextPage, refetch, fetchMore, } }