UNPKG

@shopify/shop-minis-react

Version:

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

154 lines (136 loc) 3.95 kB
import {useCallback, useEffect, useMemo, useState} from 'react' import { DataHookFetchPolicy, DataHookReturnsBase, ShopActionResult, } from '../types' import {formatError, MiniError} from '../utils/errors' export interface ShopActionsDataFetchingResult<R> extends DataHookReturnsBase { data: R | null } export const useShopActionsDataFetching = < S = unknown, P extends {fetchPolicy?: DataHookFetchPolicy} = { fetchPolicy?: DataHookFetchPolicy }, >( action: (params: P) => Promise<ShopActionResult<{data: S}>>, params: P, options: { skip?: boolean hook?: string validator?: (data: S) => void } ): ShopActionsDataFetchingResult<S> => { const [state, setState] = useState<{ data: S | null loading: boolean error: Error | null }>({ data: null, loading: true, error: null, }) const skip = options?.skip === true const {validator, hook} = options const runValidator = useCallback( (dataToValidate: S) => { try { validator?.(dataToValidate) return null } catch (err) { return ( err ?? 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, }: { setLoading?: boolean setError?: boolean resetOnError?: boolean throwOnError?: 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 => ({ ...curState, data: result.data.data, loading: false, error: validationError ?? null, })) } else { throw result.error } } catch (err) { queryError = formatError({hook}, err) } const error = validationError || queryError if (error && (setError || resetOnError)) { setState(curState => ({ data: resetOnError ? null : curState.data, 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]) useEffect(() => { if (skip) return fetch( {}, { throwOnError: false, } ) }, [fetch, skip]) return { data: state.data, loading: state.loading, error: state.error, refetch, } }