@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
text/typescript
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,
}
}