Added better mutation types, and moved some hooks

This commit is contained in:
Luis Alvarez 2021-02-18 01:10:38 -05:00
parent c02d7fec62
commit 74da22d5f4
11 changed files with 207 additions and 150 deletions

View File

@ -33,7 +33,7 @@ const CartItem = ({
currencyCode,
})
const updateItem = useUpdateItem(item)
const updateItem = useUpdateItem({ item })
const removeItem = useRemoveItem()
const [quantity, setQuantity] = useState(item.quantity)
const [removing, setRemoving] = useState(false)

View File

@ -1,29 +1,24 @@
import type { MutationHandler } from '@commerce/utils/types'
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import { normalizeCart } from '../lib/normalize'
import type {
AddCartItemBody,
Cart,
BigcommerceCart,
CartItemBody,
AddCartItemBody,
} from '../types'
import useCart from './use-cart'
import { BigcommerceProvider } from '..'
const defaultOpts = {
url: '/api/bigcommerce/cart',
method: 'POST',
}
export default useAddItem as UseAddItem<typeof handler>
export default useAddItem as UseAddItem<BigcommerceProvider, CartItemBody>
export const handler: MutationHandler<Cart, {}, AddCartItemBody> = {
export const handler: MutationHook<Cart, {}, CartItemBody> = {
fetchOptions: {
url: '/api/bigcommerce/cart',
method: 'GET',
method: 'POST',
},
async fetcher({ input: { item }, options, fetch }) {
async fetcher({ input: item, options, fetch }) {
if (
item.quantity &&
(!Number.isInteger(item.quantity) || item.quantity! < 1)
@ -34,20 +29,22 @@ export const handler: MutationHandler<Cart, {}, AddCartItemBody> = {
}
const data = await fetch<BigcommerceCart, AddCartItemBody>({
...defaultOpts,
...options,
body: { item },
})
return normalizeCart(data)
},
useHook() {
useHook: ({ fetch }) => () => {
const { mutate } = useCart()
return async function addItem({ input, fetch }) {
const data = await fetch({ input })
await mutate(data, false)
return data
}
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

@ -1,9 +1,10 @@
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import type { HookFetcher } from '@commerce/utils/types'
import type { HookContext, HookFetcherContext } from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useCartUpdateItem, {
UpdateItemInput as UseUpdateItemInput,
import useUpdateItem, {
UpdateItemInput as UpdateItemInputBase,
UseUpdateItem,
} from '@commerce/cart/use-update-item'
import { normalizeCart } from '../lib/normalize'
import type {
@ -15,49 +16,50 @@ import type {
import { fetcher as removeFetcher } from './use-remove-item'
import useCart from './use-cart'
const defaultOpts = {
url: '/api/bigcommerce/cart',
method: 'PUT',
}
export type UpdateItemInput<T = any> = T extends LineItem
? Partial<UseUpdateItemInput<LineItem>>
: UseUpdateItemInput<LineItem>
? Partial<UpdateItemInputBase<LineItem>>
: UpdateItemInputBase<LineItem>
export const fetcher: HookFetcher<Cart | null, UpdateCartItemBody> = async (
options,
{ itemId, item },
fetch
) => {
if (Number.isInteger(item.quantity)) {
// Also allow the update hook to remove an item if the quantity is lower than 1
if (item.quantity! < 1) {
return removeFetcher(null, { itemId }, fetch)
export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/bigcommerce/cart',
method: 'PUT',
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateCartItemBody>) {
if (Number.isInteger(item.quantity)) {
// Also allow the update hook to remove an item if the quantity is lower than 1
if (item.quantity! < 1) {
return removeFetcher(null, { itemId }, fetch)
}
} else if (item.quantity) {
throw new ValidationError({
message: 'The item quantity has to be a valid integer',
})
}
} else if (item.quantity) {
throw new ValidationError({
message: 'The item quantity has to be a valid integer',
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
...options,
body: { itemId, item },
})
}
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
...defaultOpts,
...options,
body: { itemId, item },
})
return normalizeCart(data)
}
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
const useUpdateItem = <T extends LineItem | undefined = undefined>(
item?: T
return normalizeCart(data)
},
useHook: ({ fetch }: HookContext<Cart | null, UpdateCartItemBody>) => <
T extends LineItem | undefined = undefined
>(
ctx: {
item?: T
wait?: number
} = {}
) => {
const { mutate } = useCart()
const fn = useCartUpdateItem<Cart | null, UpdateCartItemBody>(
defaultOpts,
customFetcher
)
const { item } = ctx
const { mutate } = useCart() as any
return useCallback(
debounce(async (input: UpdateItemInput<T>) => {
@ -71,20 +73,16 @@ function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
})
}
const data = await fn({
itemId,
item: { productId, variantId, quantity: input.quantity },
const data = await fetch({
input: {
itemId,
item: { productId, variantId, quantity: input.quantity },
},
})
await mutate(data, false)
return data
}, cfg?.wait ?? 500),
[fn, mutate]
}, ctx.wait ?? 500),
[fetch, mutate]
)
}
useUpdateItem.extend = extendHook
return useUpdateItem
},
}
export default extendHook(fetcher)

View File

@ -1,5 +1,6 @@
import { handler as useCart } from './cart/use-cart'
import { handler as useAddItem } from './cart/use-add-item'
import { handler as useUpdateItem } from './cart/use-update-item'
import { handler as useWishlist } from './wishlist/use-wishlist'
import { handler as useCustomer } from './customer/use-customer'
import { handler as useSearch } from './product/use-search'
@ -9,7 +10,7 @@ export const bigcommerceProvider = {
locale: 'en-us',
cartCookie: 'bc_cartId',
fetcher,
cart: { useCart, useAddItem },
cart: { useCart, useAddItem, useUpdateItem },
wishlist: { useWishlist },
customer: { useCustomer },
products: { useSearch },

View File

@ -43,9 +43,6 @@ export type CartItemBody = Core.CartItemBody & {
optionSelections?: OptionSelections
}
type X = Core.CartItemBody extends CartItemBody ? any : never
type Y = CartItemBody extends Core.CartItemBody ? any : never
export type GetCartHandlerBody = Core.GetCartHandlerBody
export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>

View File

@ -1,33 +1,7 @@
import { useCallback } from 'react'
import type {
Prop,
HookFetcherFn,
UseHookInput,
UseHookResponse,
} from '../utils/types'
import useHook, { useHookHandler } from '../utils/use-hook'
import type { MutationHook, HookFetcherFn } from '../utils/types'
import type { Cart, CartItemBody, AddCartItemBody } from '../types'
import { Provider, useCommerce } from '..'
import { BigcommerceProvider } from '@framework'
export type UseAddItemHandler<P extends Provider> = Prop<
Prop<P, 'cart'>,
'useAddItem'
>
// Input expected by the action returned by the `useAddItem` hook
export type UseAddItemInput<P extends Provider> = UseHookInput<
UseAddItemHandler<P>
>
export type UseAddItemResult<P extends Provider> = ReturnType<
UseHookResponse<UseAddItemHandler<P>>
>
export type UseAddItem<P extends Provider, Input> = Partial<
UseAddItemInput<P>
> extends UseAddItemInput<P>
? (input?: UseAddItemInput<P>) => (input: Input) => UseAddItemResult<P>
: (input: UseAddItemInput<P>) => (input: Input) => UseAddItemResult<P>
import type { Provider } from '..'
export const fetcher: HookFetcherFn<
Cart,
@ -36,34 +10,15 @@ export const fetcher: HookFetcherFn<
return fetch({ ...options, body: input })
}
type X = UseAddItemResult<BigcommerceProvider>
export type UseAddItem<
H extends MutationHook<any, any, any> = MutationHook<Cart, {}, CartItemBody>
> = ReturnType<H['useHook']>
export default function useAddItem<P extends Provider, Input>(
input: UseAddItemInput<P>
) {
const { providerRef, fetcherRef } = useCommerce<P>()
const fn = (provider: Provider) => provider.cart?.useAddItem!
const provider = providerRef.current
const opts = provider.cart?.useAddItem
const fetcherFn = opts?.fetcher ?? fetcher
const useHook = opts?.useHook ?? (() => () => {})
const fetchFn = provider.fetcher ?? fetcherRef.current
const action = useHook({ input })
return useCallback(
function addItem(input: Input) {
return action({
input,
fetch({ input }) {
return fetcherFn({
input,
options: opts!.fetchOptions,
fetch: fetchFn,
})
},
})
},
[input, fetchFn, opts?.fetchOptions]
)
const useAddItem: UseAddItem = (...args) => {
const handler = useHookHandler(fn, fetcher)
return handler(useHook(fn, fetcher))(...args)
}
export default useAddItem

View File

@ -1,5 +1,5 @@
import type { HookFetcher, HookFetcherOptions } from '../utils/types'
import useAddItem from './use-add-item'
// import useAddItem from './use-add-item'
import useRemoveItem from './use-remove-item'
import useUpdateItem from './use-update-item'
@ -9,9 +9,9 @@ export default function useCartActions<T, Input>(
options: HookFetcherOptions,
fetcher: HookFetcher<T, Input>
) {
const addItem = useAddItem<T, Input>(options, fetcher)
// const addItem = useAddItem<T, Input>(options, fetcher)
const updateItem = useUpdateItem<T, Input>(options, fetcher)
const removeItem = useRemoveItem<T, Input>(options, fetcher)
return { addItem, updateItem, removeItem }
return { updateItem, removeItem }
}

View File

@ -1,11 +1,39 @@
import useAction from '../utils/use-action'
import type { CartItemBody } from '../types'
import useHook, { useHookHandler } from '../utils/use-hook'
import type { MutationHook, HookFetcherFn } from '../utils/types'
import type { Cart, CartItemBody, LineItem, UpdateCartItemBody } from '../types'
import type { Provider } from '..'
import debounce from 'lodash.debounce'
// Input expected by the action returned by the `useUpdateItem` hook
export type UpdateItemInput<T extends CartItemBody> = T & {
id: string
}
const useUpdateItem = useAction
export type UseUpdateItem<
H extends MutationHook<any, any, any> = MutationHook<
Cart,
{
item?: LineItem
wait?: number
},
UpdateItemInput<CartItemBody>,
UpdateCartItemBody<CartItemBody>
>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<any> = async ({
options,
input,
fetch,
}) => {
return fetch({ ...options, body: input })
}
const fn = (provider: Provider) => provider.cart?.useUpdateItem!
const useUpdateItem: UseUpdateItem = (input = {}) => {
const handler = useHookHandler(fn, fetcher)
return debounce(handler(useHook(fn, fetcher))(input), input.wait ?? 500)
}
export default useUpdateItem

View File

@ -6,7 +6,12 @@ import {
useMemo,
useRef,
} from 'react'
import { Fetcher, HookHandler, MutationHandler } from './utils/types'
import {
Fetcher,
HookHandler,
MutationHandler,
MutationHook,
} from './utils/types'
import type { FetchCartInput } from './cart/use-cart'
import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
@ -16,7 +21,9 @@ export type Provider = CommerceConfig & {
fetcher: Fetcher
cart?: {
useCart?: HookHandler<Cart | null, any, FetchCartInput>
useAddItem?: MutationHandler<Cart, any, any>
useAddItem?: MutationHook<any, any, any>
useUpdateItem?: MutationHook<any, any, any>
useRemoveItem?: MutationHook<any, any, any>
}
wishlist?: {
useWishlist?: HookHandler<Wishlist | null, any, any>

View File

@ -1,3 +1,4 @@
import { LineItem } from '@framework/types'
import type { ConfigInterface } from 'swr'
import type { CommerceError } from './errors'
import type { ResponseState } from './use-data'
@ -31,16 +32,15 @@ export type HookFetcher<Data, Input = null, Result = any> = (
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
) => Data | Promise<Data>
export type HookFetcherFn<
Data,
Input = never,
Result = any,
Body = any
> = (context: {
export type HookFetcherFn<Data, Input = never, Result = any, Body = any> = (
context: HookFetcherContext<Input, Result, Body>
) => Data | Promise<Data>
export type HookFetcherContext<Input = never, Result = any, Body = any> = {
options: HookFetcherOptions
input: Input
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
}) => Data | Promise<Data>
}
export type HookFetcherOptions = { method?: string } & (
| { query: string; url?: string }
@ -53,8 +53,6 @@ export type HookSwrInput = [string, HookInputValue][]
export type HookFetchInput = { [k: string]: HookInputValue }
export type HookInput = {}
export type HookHandler<
// Data obj returned by the hook and fetch operation
Data,
@ -94,6 +92,39 @@ export type MutationHandler<
fetcher?: HookFetcherFn<Data, FetchInput>
}
export type HookFunction<
Input extends { [k: string]: unknown } | {},
T
> = keyof Input extends never
? () => T
: Partial<Input> extends Input
? (input?: Input) => T
: (input: Input) => T
export type MutationHook<
// Data obj returned by the hook and fetch operation
Data,
// Input expected by the hook
Input extends { [k: string]: unknown } = {},
// Input expected by the action returned by the hook
ActionInput extends { [k: string]: unknown } = {},
// Input expected before doing a fetch operation
FetchInput extends { [k: string]: unknown } = ActionInput
> = {
useHook(
context: HookContext<Data, FetchInput>
): HookFunction<Input, HookFunction<ActionInput, Data | Promise<Data>>>
fetchOptions: HookFetcherOptions
fetcher?: HookFetcherFn<Data, FetchInput>
}
export type HookContext<
Data,
FetchInput extends { [k: string]: unknown } = {}
> = {
fetch: (context: { input: FetchInput }) => Data | Promise<Data>
}
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
Data,
CommerceError,

View File

@ -0,0 +1,43 @@
import { useCallback } from 'react'
import type { MutationHook } from './types'
import { Provider, useCommerce } from '..'
export function useHookHandler<P extends Provider>(
fn: (provider: P) => MutationHook<any, any, any>,
fetcher: any
) {
const { providerRef } = useCommerce<P>()
const provider = providerRef.current
const opts = fn(provider)
const handler =
opts.useHook ??
(() => {
const { fetch } = useHook(fn, fetcher)
return (input: any) => fetch({ input })
})
return handler
}
export default function useHook<P extends Provider>(
fn: (provider: P) => MutationHook<any, any, any>,
fetcher: any
) {
const { providerRef, fetcherRef } = useCommerce<P>()
const provider = providerRef.current
const opts = fn(provider)
const fetcherFn = opts.fetcher ?? fetcher
const fetchFn = provider.fetcher ?? fetcherRef.current
const fetch = useCallback(
({ input }: { input: any }) => {
return fetcherFn({
input,
options: opts.fetchOptions,
fetch: fetchFn,
})
},
[fetchFn, opts.fetchOptions]
)
return { fetch }
}