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