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

View File

@ -1,9 +1,10 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import debounce from 'lodash.debounce' 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 { ValidationError } from '@commerce/utils/errors'
import useCartUpdateItem, { import useUpdateItem, {
UpdateItemInput as UseUpdateItemInput, UpdateItemInput as UpdateItemInputBase,
UseUpdateItem,
} from '@commerce/cart/use-update-item' } from '@commerce/cart/use-update-item'
import { normalizeCart } from '../lib/normalize' import { normalizeCart } from '../lib/normalize'
import type { import type {
@ -15,20 +16,22 @@ import type {
import { fetcher as removeFetcher } from './use-remove-item' import { fetcher as removeFetcher } from './use-remove-item'
import useCart from './use-cart' import useCart from './use-cart'
const defaultOpts = { export type UpdateItemInput<T = any> = T extends LineItem
? Partial<UpdateItemInputBase<LineItem>>
: UpdateItemInputBase<LineItem>
export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/bigcommerce/cart', url: '/api/bigcommerce/cart',
method: 'PUT', method: 'PUT',
} },
async fetcher({
export type UpdateItemInput<T = any> = T extends LineItem input: { itemId, item },
? Partial<UseUpdateItemInput<LineItem>>
: UseUpdateItemInput<LineItem>
export const fetcher: HookFetcher<Cart | null, UpdateCartItemBody> = async (
options, options,
{ itemId, item }, fetch,
fetch }: HookFetcherContext<UpdateCartItemBody>) {
) => {
if (Number.isInteger(item.quantity)) { if (Number.isInteger(item.quantity)) {
// Also allow the update hook to remove an item if the quantity is lower than 1 // Also allow the update hook to remove an item if the quantity is lower than 1
if (item.quantity! < 1) { if (item.quantity! < 1) {
@ -41,23 +44,22 @@ export const fetcher: HookFetcher<Cart | null, UpdateCartItemBody> = async (
} }
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({ const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
...defaultOpts,
...options, ...options,
body: { itemId, item }, body: { itemId, item },
}) })
return normalizeCart(data) return normalizeCart(data)
} },
useHook: ({ fetch }: HookContext<Cart | null, UpdateCartItemBody>) => <
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) { T extends LineItem | undefined = undefined
const useUpdateItem = <T extends LineItem | undefined = undefined>( >(
ctx: {
item?: T item?: T
wait?: number
} = {}
) => { ) => {
const { mutate } = useCart() const { item } = ctx
const fn = useCartUpdateItem<Cart | null, UpdateCartItemBody>( const { mutate } = useCart() as any
defaultOpts,
customFetcher
)
return useCallback( return useCallback(
debounce(async (input: UpdateItemInput<T>) => { debounce(async (input: UpdateItemInput<T>) => {
@ -71,20 +73,16 @@ function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
}) })
} }
const data = await fn({ const data = await fetch({
input: {
itemId, itemId,
item: { productId, variantId, quantity: input.quantity }, item: { productId, variantId, quantity: input.quantity },
},
}) })
await mutate(data, false) await mutate(data, false)
return data return data
}, cfg?.wait ?? 500), }, ctx.wait ?? 500),
[fn, mutate] [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 useCart } from './cart/use-cart'
import { handler as useAddItem } from './cart/use-add-item' 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 useWishlist } from './wishlist/use-wishlist'
import { handler as useCustomer } from './customer/use-customer' import { handler as useCustomer } from './customer/use-customer'
import { handler as useSearch } from './product/use-search' import { handler as useSearch } from './product/use-search'
@ -9,7 +10,7 @@ export const bigcommerceProvider = {
locale: 'en-us', locale: 'en-us',
cartCookie: 'bc_cartId', cartCookie: 'bc_cartId',
fetcher, fetcher,
cart: { useCart, useAddItem }, cart: { useCart, useAddItem, useUpdateItem },
wishlist: { useWishlist }, wishlist: { useWishlist },
customer: { useCustomer }, customer: { useCustomer },
products: { useSearch }, products: { useSearch },

View File

@ -43,9 +43,6 @@ export type CartItemBody = Core.CartItemBody & {
optionSelections?: OptionSelections 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 GetCartHandlerBody = Core.GetCartHandlerBody
export type AddCartItemBody = Core.AddCartItemBody<CartItemBody> export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>

View File

@ -1,33 +1,7 @@
import { useCallback } from 'react' import useHook, { useHookHandler } from '../utils/use-hook'
import type { import type { MutationHook, HookFetcherFn } from '../utils/types'
Prop,
HookFetcherFn,
UseHookInput,
UseHookResponse,
} from '../utils/types'
import type { Cart, CartItemBody, AddCartItemBody } from '../types' import type { Cart, CartItemBody, AddCartItemBody } from '../types'
import { Provider, useCommerce } from '..' import type { Provider } 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>
export const fetcher: HookFetcherFn< export const fetcher: HookFetcherFn<
Cart, Cart,
@ -36,34 +10,15 @@ export const fetcher: HookFetcherFn<
return fetch({ ...options, body: input }) 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>( const fn = (provider: Provider) => provider.cart?.useAddItem!
input: UseAddItemInput<P>
) {
const { providerRef, fetcherRef } = useCommerce<P>()
const provider = providerRef.current const useAddItem: UseAddItem = (...args) => {
const opts = provider.cart?.useAddItem const handler = useHookHandler(fn, fetcher)
return handler(useHook(fn, fetcher))(...args)
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]
)
} }
export default useAddItem

View File

@ -1,5 +1,5 @@
import type { HookFetcher, HookFetcherOptions } from '../utils/types' 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 useRemoveItem from './use-remove-item'
import useUpdateItem from './use-update-item' import useUpdateItem from './use-update-item'
@ -9,9 +9,9 @@ export default function useCartActions<T, Input>(
options: HookFetcherOptions, options: HookFetcherOptions,
fetcher: HookFetcher<T, Input> 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 updateItem = useUpdateItem<T, Input>(options, fetcher)
const removeItem = useRemoveItem<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 useHook, { useHookHandler } from '../utils/use-hook'
import type { CartItemBody } from '../types' 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 // Input expected by the action returned by the `useUpdateItem` hook
export type UpdateItemInput<T extends CartItemBody> = T & { export type UpdateItemInput<T extends CartItemBody> = T & {
id: string 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 export default useUpdateItem

View File

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

View File

@ -1,3 +1,4 @@
import { LineItem } from '@framework/types'
import type { ConfigInterface } from 'swr' import type { ConfigInterface } from 'swr'
import type { CommerceError } from './errors' import type { CommerceError } from './errors'
import type { ResponseState } from './use-data' 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> fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
) => Data | Promise<Data> ) => Data | Promise<Data>
export type HookFetcherFn< export type HookFetcherFn<Data, Input = never, Result = any, Body = any> = (
Data, context: HookFetcherContext<Input, Result, Body>
Input = never, ) => Data | Promise<Data>
Result = any,
Body = any export type HookFetcherContext<Input = never, Result = any, Body = any> = {
> = (context: {
options: HookFetcherOptions options: HookFetcherOptions
input: Input input: Input
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T> fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
}) => Data | Promise<Data> }
export type HookFetcherOptions = { method?: string } & ( export type HookFetcherOptions = { method?: string } & (
| { query: string; url?: string } | { query: string; url?: string }
@ -53,8 +53,6 @@ export type HookSwrInput = [string, HookInputValue][]
export type HookFetchInput = { [k: string]: HookInputValue } export type HookFetchInput = { [k: string]: HookInputValue }
export type HookInput = {}
export type HookHandler< export type HookHandler<
// Data obj returned by the hook and fetch operation // Data obj returned by the hook and fetch operation
Data, Data,
@ -94,6 +92,39 @@ export type MutationHandler<
fetcher?: HookFetcherFn<Data, FetchInput> 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< export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
Data, Data,
CommerceError, 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 }
}