From 2e1d2610bb26876e493d22655c4abd30dc51f511 Mon Sep 17 00:00:00 2001
From: Luis Alvarez <luis@vercel.com>
Date: Thu, 18 Feb 2021 23:22:45 -0500
Subject: [PATCH] Moved more hooks and updated types to make them smaller

---
 framework/bigcommerce/cart/use-cart.tsx       | 11 ++-
 .../bigcommerce/cart/use-remove-item.tsx      |  2 +-
 framework/commerce/cart/use-add-item.tsx      | 11 ++-
 framework/commerce/cart/use-cart.tsx          | 64 +++++-----------
 framework/commerce/cart/use-remove-item.tsx   | 13 ++--
 framework/commerce/cart/use-update-item.tsx   | 13 ++--
 framework/commerce/index.tsx                  |  4 +-
 framework/commerce/utils/default-fetcher.ts   |  2 +-
 framework/commerce/utils/types.ts             | 38 +++++++++-
 framework/commerce/utils/use-data.tsx         |  9 ++-
 framework/commerce/utils/use-hook.ts          | 73 ++++++++++---------
 11 files changed, 128 insertions(+), 112 deletions(-)

diff --git a/framework/bigcommerce/cart/use-cart.tsx b/framework/bigcommerce/cart/use-cart.tsx
index b5cc0cccf..2098e7431 100644
--- a/framework/bigcommerce/cart/use-cart.tsx
+++ b/framework/bigcommerce/cart/use-cart.tsx
@@ -1,13 +1,12 @@
 import { useMemo } from 'react'
-import { HookHandler } from '@commerce/utils/types'
+import { SWRHook } from '@commerce/utils/types'
 import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart'
 import { normalizeCart } from '../lib/normalize'
 import type { Cart } from '../types'
-import type { BigcommerceProvider } from '..'
 
-export default useCart as UseCart<BigcommerceProvider>
+export default useCart as UseCart<typeof handler>
 
-export const handler: HookHandler<
+export const handler: SWRHook<
   Cart | null,
   {},
   FetchCartInput,
@@ -21,9 +20,9 @@ export const handler: HookHandler<
     const data = cartId ? await fetch(options) : null
     return data && normalizeCart(data)
   },
-  useHook({ input, useData }) {
+  useHook: ({ useData }) => (input) => {
     const response = useData({
-      swrOptions: { revalidateOnFocus: false, ...input.swrOptions },
+      swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
     })
 
     return useMemo(
diff --git a/framework/bigcommerce/cart/use-remove-item.tsx b/framework/bigcommerce/cart/use-remove-item.tsx
index c1f1ab4c0..f279b2dc6 100644
--- a/framework/bigcommerce/cart/use-remove-item.tsx
+++ b/framework/bigcommerce/cart/use-remove-item.tsx
@@ -46,7 +46,7 @@ export const handler = {
     ctx: { item?: T } = {}
   ) => {
     const { item } = ctx
-    const { mutate } = useCart() as any
+    const { mutate } = useCart()
     const removeItem: RemoveItemFn<LineItem> = async (input) => {
       const itemId = input?.id ?? item?.id
 
diff --git a/framework/commerce/cart/use-add-item.tsx b/framework/commerce/cart/use-add-item.tsx
index 715029d18..13da6b416 100644
--- a/framework/commerce/cart/use-add-item.tsx
+++ b/framework/commerce/cart/use-add-item.tsx
@@ -1,4 +1,5 @@
-import useHook, { useHookHandler } from '../utils/use-hook'
+import { useHook, useMutationHook } from '../utils/use-hook'
+import { mutationFetcher } from '../utils/default-fetcher'
 import type { MutationHook, HookFetcherFn } from '../utils/types'
 import type { Cart, CartItemBody, AddCartItemBody } from '../types'
 import type { Provider } from '..'
@@ -6,9 +7,7 @@ import type { Provider } from '..'
 export const fetcher: HookFetcherFn<
   Cart,
   AddCartItemBody<CartItemBody>
-> = async ({ options, input, fetch }) => {
-  return fetch({ ...options, body: input })
-}
+> = mutationFetcher
 
 export type UseAddItem<
   H extends MutationHook<any, any, any> = MutationHook<Cart, {}, CartItemBody>
@@ -17,8 +16,8 @@ export type UseAddItem<
 const fn = (provider: Provider) => provider.cart?.useAddItem!
 
 const useAddItem: UseAddItem = (...args) => {
-  const handler = useHookHandler(fn, fetcher)
-  return handler(useHook(fn, fetcher))(...args)
+  const hook = useHook(fn)
+  return useMutationHook({ fetcher, ...hook })(...args)
 }
 
 export default useAddItem
diff --git a/framework/commerce/cart/use-cart.tsx b/framework/commerce/cart/use-cart.tsx
index f7b384047..6053c91d7 100644
--- a/framework/commerce/cart/use-cart.tsx
+++ b/framework/commerce/cart/use-cart.tsx
@@ -1,34 +1,21 @@
 import Cookies from 'js-cookie'
 import type { Cart } from '../types'
-import type {
-  Prop,
-  HookFetcherFn,
-  UseHookInput,
-  UseHookResponse,
-} from '../utils/types'
-import useData from '../utils/use-data'
+import type { HookFetcherFn, SWRHook } from '../utils/types'
 import { Provider, useCommerce } from '..'
+import { useHook, useSWRHook } from '@commerce/utils/use-hook'
 
 export type FetchCartInput = {
   cartId?: Cart['id']
 }
 
-export type UseCartHandler<P extends Provider> = Prop<
-  Prop<P, 'cart'>,
-  'useCart'
->
-
-export type UseCartInput<P extends Provider> = UseHookInput<UseCartHandler<P>>
-
-export type CartResponse<P extends Provider> = UseHookResponse<
-  UseCartHandler<P>
->
-
-export type UseCart<P extends Provider> = Partial<
-  UseCartInput<P>
-> extends UseCartInput<P>
-  ? (input?: UseCartInput<P>) => CartResponse<P>
-  : (input: UseCartInput<P>) => CartResponse<P>
+export type UseCart<
+  H extends SWRHook<any, any, any> = SWRHook<
+    Cart | null,
+    {},
+    FetchCartInput,
+    { isEmpty?: boolean }
+  >
+> = ReturnType<H['useHook']>
 
 export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
   options,
@@ -38,32 +25,17 @@ export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
   return cartId ? await fetch({ ...options }) : null
 }
 
-export default function useCart<P extends Provider>(
-  input: UseCartInput<P> = {}
-) {
-  const { providerRef, fetcherRef, cartCookie } = useCommerce<P>()
-
-  const provider = providerRef.current
-  const opts = provider.cart?.useCart
-
-  const fetcherFn = opts?.fetcher ?? fetcher
-  const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
+const fn = (provider: Provider) => provider.cart?.useCart!
 
+const useCart: UseCart = (input) => {
+  const hook = useHook(fn)
+  const { cartCookie } = useCommerce()
+  const fetcherFn = hook.fetcher ?? fetcher
   const wrapper: typeof fetcher = (context) => {
     context.input.cartId = Cookies.get(cartCookie)
     return fetcherFn(context)
   }
-
-  return useHook({
-    input,
-    useData(ctx) {
-      const response = useData(
-        { ...opts!, fetcher: wrapper },
-        ctx?.input ?? [],
-        provider.fetcher ?? fetcherRef.current,
-        ctx?.swrOptions ?? input.swrOptions
-      )
-      return response
-    },
-  })
+  return useSWRHook({ ...hook, fetcher: wrapper })(input)
 }
+
+export default useCart
diff --git a/framework/commerce/cart/use-remove-item.tsx b/framework/commerce/cart/use-remove-item.tsx
index 32d8cbf1c..a9d1b37d2 100644
--- a/framework/commerce/cart/use-remove-item.tsx
+++ b/framework/commerce/cart/use-remove-item.tsx
@@ -1,6 +1,6 @@
-import useHook, { useHookHandler } from '../utils/use-hook'
+import { useHook, useMutationHook } from '../utils/use-hook'
 import { mutationFetcher } from '../utils/default-fetcher'
-import type { MutationHook } from '../utils/types'
+import type { HookFetcherFn, MutationHook } from '../utils/types'
 import type { Cart, LineItem, RemoveCartItemBody } from '../types'
 import type { Provider } from '..'
 
@@ -20,13 +20,16 @@ export type UseRemoveItem<
   >
 > = ReturnType<H['useHook']>
 
-export const fetcher = mutationFetcher
+export const fetcher: HookFetcherFn<
+  Cart | null,
+  RemoveCartItemBody
+> = mutationFetcher
 
 const fn = (provider: Provider) => provider.cart?.useRemoveItem!
 
 const useRemoveItem: UseRemoveItem = (input) => {
-  const handler = useHookHandler(fn, fetcher)
-  return handler(useHook(fn, fetcher))(input)
+  const hook = useHook(fn)
+  return useMutationHook({ fetcher, ...hook })(input)
 }
 
 export default useRemoveItem
diff --git a/framework/commerce/cart/use-update-item.tsx b/framework/commerce/cart/use-update-item.tsx
index 93afdbb1e..f8d0f1a40 100644
--- a/framework/commerce/cart/use-update-item.tsx
+++ b/framework/commerce/cart/use-update-item.tsx
@@ -1,6 +1,6 @@
-import useHook, { useHookHandler } from '../utils/use-hook'
+import { useHook, useMutationHook } from '../utils/use-hook'
 import { mutationFetcher } from '../utils/default-fetcher'
-import type { MutationHook } from '../utils/types'
+import type { HookFetcherFn, MutationHook } from '../utils/types'
 import type { Cart, CartItemBody, LineItem, UpdateCartItemBody } from '../types'
 import type { Provider } from '..'
 
@@ -23,13 +23,16 @@ export type UseUpdateItem<
   >
 > = ReturnType<H['useHook']>
 
-export const fetcher = mutationFetcher
+export const fetcher: HookFetcherFn<
+  Cart | null,
+  UpdateCartItemBody<CartItemBody>
+> = mutationFetcher
 
 const fn = (provider: Provider) => provider.cart?.useUpdateItem!
 
 const useUpdateItem: UseUpdateItem = (input) => {
-  const handler = useHookHandler(fn, fetcher)
-  return handler(useHook(fn, fetcher))(input)
+  const hook = useHook(fn)
+  return useMutationHook({ fetcher, ...hook })(input)
 }
 
 export default useUpdateItem
diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx
index 1a2ba878b..6ce2c8176 100644
--- a/framework/commerce/index.tsx
+++ b/framework/commerce/index.tsx
@@ -6,7 +6,7 @@ import {
   useMemo,
   useRef,
 } from 'react'
-import { Fetcher, HookHandler, MutationHook } from './utils/types'
+import { Fetcher, HookHandler, SWRHook, MutationHook } from './utils/types'
 import type { FetchCartInput } from './cart/use-cart'
 import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
 
@@ -15,7 +15,7 @@ const Commerce = createContext<CommerceContextValue<any> | {}>({})
 export type Provider = CommerceConfig & {
   fetcher: Fetcher
   cart?: {
-    useCart?: HookHandler<Cart | null, any, FetchCartInput>
+    useCart?: SWRHook<Cart | null, any, FetchCartInput>
     useAddItem?: MutationHook<any, any, any>
     useUpdateItem?: MutationHook<any, any, any>
     useRemoveItem?: MutationHook<any, any, any>
diff --git a/framework/commerce/utils/default-fetcher.ts b/framework/commerce/utils/default-fetcher.ts
index cdaf05516..654da2499 100644
--- a/framework/commerce/utils/default-fetcher.ts
+++ b/framework/commerce/utils/default-fetcher.ts
@@ -3,7 +3,7 @@ import type { HookFetcherFn } from './types'
 const defaultFetcher: HookFetcherFn<any> = ({ options, fetch }) =>
   fetch(options)
 
-export const mutationFetcher: HookFetcherFn<any> = ({
+export const mutationFetcher: HookFetcherFn<any, any> = ({
   input,
   options,
   fetch,
diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts
index 98e4d0f34..01f1c2eb3 100644
--- a/framework/commerce/utils/types.ts
+++ b/framework/commerce/utils/types.ts
@@ -9,7 +9,9 @@ export type Override<T, K> = Omit<T, keyof K> & K
  * Returns the properties in T with the properties in type K changed from optional to required
  */
 export type PickRequired<T, K extends keyof T> = Omit<T, K> &
-  Required<Pick<T, K>>
+  {
+    [P in K]-?: NonNullable<T[P]>
+  }
 
 /**
  * Core fetcher added by CommerceProvider
@@ -83,6 +85,36 @@ export type HookFunction<
   ? (input?: Input) => T
   : (input: Input) => T
 
+export type SWRHook<
+  // Data obj returned by the hook and fetch operation
+  Data,
+  // Input expected by the hook
+  Input extends { [k: string]: unknown } = {},
+  // Input expected before doing a fetch operation
+  FetchInput extends HookFetchInput = {},
+  // Custom state added to the response object of SWR
+  State = {}
+> = {
+  useHook(
+    context: SWRHookContext<Data, FetchInput>
+  ): HookFunction<
+    Input & { swrOptions?: SwrOptions<Data, FetchInput> },
+    ResponseState<Data> & State
+  >
+  fetchOptions: HookFetcherOptions
+  fetcher?: HookFetcherFn<Data, FetchInput>
+}
+
+export type SWRHookContext<
+  Data,
+  FetchInput extends { [k: string]: unknown } = {}
+> = {
+  useData(context?: {
+    input?: HookFetchInput | HookSwrInput
+    swrOptions?: SwrOptions<Data, FetchInput>
+  }): ResponseState<Data>
+}
+
 export type MutationHook<
   // Data obj returned by the hook and fetch operation
   Data,
@@ -118,9 +150,7 @@ export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
  */
 export type Prop<T, K extends keyof T> = NonNullable<T[K]>
 
-export type HookHandlerType =
-  | HookHandler<any, any, any>
-  | MutationHandler<any, any, any>
+export type HookHandlerType = HookHandler<any, any, any>
 
 export type UseHookParameters<H extends HookHandlerType> = Parameters<
   Prop<H, 'useHook'>
diff --git a/framework/commerce/utils/use-data.tsx b/framework/commerce/utils/use-data.tsx
index 94679a0c6..fe21bd3d3 100644
--- a/framework/commerce/utils/use-data.tsx
+++ b/framework/commerce/utils/use-data.tsx
@@ -1,11 +1,11 @@
 import useSWR, { responseInterface } from 'swr'
 import type {
-  HookHandler,
   HookSwrInput,
   HookFetchInput,
-  PickRequired,
   Fetcher,
   SwrOptions,
+  HookFetcherOptions,
+  HookFetcherFn,
 } from './types'
 import defineProperty from './define-property'
 import { CommerceError } from './errors'
@@ -19,7 +19,10 @@ export type UseData = <
   Input extends { [k: string]: unknown } = {},
   FetchInput extends HookFetchInput = {}
 >(
-  options: PickRequired<HookHandler<Data, Input, FetchInput>, 'fetcher'>,
+  options: {
+    fetchOptions: HookFetcherOptions
+    fetcher: HookFetcherFn<Data, FetchInput>
+  },
   input: HookFetchInput | HookSwrInput,
   fetcherFn: Fetcher,
   swrOptions?: SwrOptions<Data, FetchInput>
diff --git a/framework/commerce/utils/use-hook.ts b/framework/commerce/utils/use-hook.ts
index b37c33370..830918de5 100644
--- a/framework/commerce/utils/use-hook.ts
+++ b/framework/commerce/utils/use-hook.ts
@@ -1,43 +1,50 @@
 import { useCallback } from 'react'
-import type { MutationHook } from './types'
+import type { Fetcher, MutationHook, PickRequired, SWRHook } from './types'
 import { Provider, useCommerce } from '..'
+import useData from './use-data'
 
-export function useHookHandler<P extends Provider>(
-  fn: (provider: P) => MutationHook<any, any, any>,
-  fetcher: any
-) {
+export function useFetcher() {
+  const { providerRef, fetcherRef } = useCommerce()
+  return providerRef.current.fetcher ?? fetcherRef.current
+}
+
+export function useHook<
+  P extends Provider,
+  H extends MutationHook<any, any, any> | SWRHook<any, any, any>
+>(fn: (provider: P) => H) {
   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
+  return fn(provider)
 }
 
-export default function useHook<P extends Provider>(
-  fn: (provider: P) => MutationHook<any, any, any>,
-  fetcher: any
+export function useSWRHook<H extends SWRHook<any, any, any>>(
+  hook: PickRequired<H, 'fetcher'>
 ) {
-  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]
-  )
+  const fetcher = useFetcher()
 
-  return { fetch }
+  return hook.useHook({
+    useData(ctx) {
+      const response = useData(hook, ctx?.input ?? [], fetcher, ctx?.swrOptions)
+      return response
+    },
+  })
+}
+
+export function useMutationHook<H extends MutationHook<any, any, any>>(
+  hook: PickRequired<H, 'fetcher'>
+) {
+  const fetcher = useFetcher()
+
+  return hook.useHook({
+    fetch: useCallback(
+      ({ input }) => {
+        return hook.fetcher({
+          input,
+          options: hook.fetchOptions,
+          fetch: fetcher,
+        })
+      },
+      [fetcher, hook.fetchOptions]
+    ),
+  })
 }