MOving stuff around and adding temporal new files

This commit is contained in:
Luis Alvarez 2021-02-05 17:44:10 -05:00
parent 1898f094bc
commit 2c9b8b100d
6 changed files with 261 additions and 11 deletions

View File

@ -1,11 +1,16 @@
import { ReactNode } from 'react'
import * as React from 'react'
import { Fetcher } from '@commerce/utils/types'
import {
CommerceConfig,
CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce,
HookHandler,
} from '@commerce'
import { FetcherError } from '@commerce/utils/errors'
import type { CartInput } from '@commerce/cart/use-cart'
import { normalizeCart } from './lib/normalize'
import { Cart } from './types'
async function getText(res: Response) {
try {
@ -23,6 +28,57 @@ async function getError(res: Response) {
return new FetcherError({ message: await getText(res), status: res.status })
}
const fetcher: Fetcher<any> = async ({
url,
method = 'GET',
variables,
body: bodyObj,
}) => {
const hasBody = Boolean(variables || bodyObj)
const body = hasBody
? JSON.stringify(variables ? { variables } : bodyObj)
: undefined
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
const res = await fetch(url!, { method, body, headers })
if (res.ok) {
const { data } = await res.json()
return data
}
throw await getError(res)
}
const useCart: HookHandler<Cart, CartInput> = {
fetchOptions: {
url: '/api/bigcommerce/cart',
method: 'GET',
},
fetcher(context) {
return undefined as any
},
onResponse(response) {
return Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems.length ?? 0) <= 0
},
enumerable: true,
},
})
},
}
export const bigcommerceProvider = {
locale: 'en-us',
cartCookie: 'bc_cartId',
fetcher,
cartNormalizer: normalizeCart,
cart: { useCart },
}
export type BigcommerceProvider = typeof bigcommerceProvider
export const bigcommerceConfig: CommerceConfig = {
locale: 'en-us',
cartCookie: 'bc_cartId',
@ -52,10 +108,13 @@ export type BigcommerceProps = {
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
return (
<CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
<CoreCommerceProvider
provider={bigcommerceProvider}
config={{ ...bigcommerceConfig, ...config }}
>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce()
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()

View File

@ -0,0 +1,28 @@
import Cookies from 'js-cookie'
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
import useData, { ResponseState, SwrOptions } from '../utils/use-data'
import type { Cart } from '../types'
import { useCommerce } from '..'
export type CartResponse<Data> = ResponseState<Data> & { isEmpty?: boolean }
// Input expected by the `useCart` hook
export type CartInput = {
cartId?: Cart['id']
}
export default function useCart<Data extends Cart | null>(
options: HookFetcherOptions,
input: HookInput,
fetcherFn: HookFetcher<Data, CartInput>,
swrOptions?: SwrOptions<Data, CartInput>
): CartResponse<Data> {
const { providerRef, cartCookie } = useCommerce()
const fetcher: typeof fetcherFn = (options, input, fetch) => {
input.cartId = Cookies.get(cartCookie)
return fetcherFn(options, input, fetch)
}
const response = useData(options, input, fetcher, swrOptions)
return response
}

View File

@ -0,0 +1,42 @@
import { useMemo } from 'react'
import Cookies from 'js-cookie'
import type { Cart } from '../types'
import type { HookFetcherFn } from '../utils/types'
import useData from '../utils/use-data-2'
import { Provider, useCommerce } from '..'
// Input expected by the `useCart` hook
export type CartInput = {
cartId?: Cart['id']
}
const fetcher: HookFetcherFn<Cart | null, CartInput> = async ({
options,
input: { cartId },
fetch,
normalize,
}) => {
const data = cartId ? await fetch({ ...options }) : null
return data && normalize ? normalize(data) : data
}
export default function useFake<P extends Provider>() {
const { providerRef, cartCookie } = useCommerce<P>()
const provider = providerRef.current
const opts = provider.cart?.useCart
const options = opts?.fetchOptions ?? {}
const fetcherFn = opts?.fetcher ?? fetcher
const wrapper: typeof fetcher = (context) => {
context.input.cartId = Cookies.get(cartCookie)
return fetcherFn(context)
}
const response = useData(options, [], wrapper, opts?.swrOptions)
const memoizedResponse = useMemo(
() => (opts?.onResponse ? opts.onResponse(response) : response),
[response]
)
return memoizedResponse
}

View File

@ -7,36 +7,71 @@ import {
useRef,
} from 'react'
import * as React from 'react'
import { Fetcher } from './utils/types'
import { Fetcher, HookFetcherFn, HookFetcherOptions } from './utils/types'
import { Cart } from './types'
import type { ResponseState, SwrOptions } from './utils/use-data'
import type { CartInput } from './cart/use-cart'
const Commerce = createContext<CommerceContextValue | {}>({})
const Commerce = createContext<CommerceContextValue<any> | {}>({})
export type CommerceProps = {
export type Provider = CommerceConfig & {
cart?: {
useCart?: HookHandler<Cart, CartInput>
}
cartNormalizer(data: any): Cart
}
export type HookHandler<Data, Input, Result = any, Body = any> = {
swrOptions?: SwrOptions<Data | null, Input, Result>
onResponse?(response: ResponseState<Data | null>): ResponseState<Data | null>
onMutation?: any
fetchOptions?: HookFetcherOptions
} & (
| // TODO: Maybe the normalizer is not required if it's used by the API route directly?
{
fetcher: HookFetcherFn<Data | null, Input, Result, Body>
normalizer?(data: Result): Data
}
| {
fetcher?: never
normalizer(data: Result): Data
}
)
export type CommerceProps<P extends Provider> = {
children?: ReactNode
provider: P
config: CommerceConfig
}
export type CommerceConfig = { fetcher: Fetcher<any> } & Omit<
CommerceContextValue,
'fetcherRef'
CommerceContextValue<any>,
'providerRef' | 'fetcherRef'
>
export type CommerceContextValue = {
export type CommerceContextValue<P extends Provider> = {
providerRef: MutableRefObject<P>
fetcherRef: MutableRefObject<Fetcher<any>>
locale: string
cartCookie: string
}
export function CommerceProvider({ children, config }: CommerceProps) {
export function CommerceProvider<P extends Provider>({
provider,
children,
config,
}: CommerceProps<P>) {
if (!config) {
throw new Error('CommerceProvider requires a valid config object')
}
const providerRef = useRef(provider)
const fetcherRef = useRef(config.fetcher)
// Because the config is an object, if the parent re-renders this provider
// will re-render every consumer unless we memoize the config
const cfg = useMemo(
() => ({
providerRef,
fetcherRef,
locale: config.locale,
cartCookie: config.cartCookie,
@ -47,6 +82,6 @@ export function CommerceProvider({ children, config }: CommerceProps) {
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
}
export function useCommerce<T extends CommerceContextValue>() {
return useContext(Commerce) as T
export function useCommerce<P extends Provider>() {
return useContext(Commerce) as CommerceContextValue<P>
}

View File

@ -15,6 +15,13 @@ 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, Result = any, Body = any> = (context: {
options: HookFetcherOptions | null
input: Input
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
normalize?(data: Result): Data
}) => Data | Promise<Data>
export type HookFetcherOptions = {
query?: string
url?: string

View File

@ -0,0 +1,79 @@
import useSWR, { ConfigInterface, responseInterface } from 'swr'
import type {
HookInput,
HookFetcher,
HookFetcherOptions,
HookFetcherFn,
} from './types'
import defineProperty from './define-property'
import { CommerceError } from './errors'
import { useCommerce } from '..'
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
Data,
CommerceError,
HookFetcher<Data, Input, Result>
>
export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
isLoading: boolean
}
export type UseData = <Data = any, Input = null, Result = any>(
options: HookFetcherOptions | (() => HookFetcherOptions | null),
input: HookInput,
fetcherFn: HookFetcherFn<Data, Input, Result>,
swrOptions?: SwrOptions<Data, Input, Result>
) => ResponseState<Data>
const useData: UseData = (options, input, fetcherFn, swrOptions) => {
const { fetcherRef } = useCommerce()
const fetcher = async (
url?: string,
query?: string,
method?: string,
...args: any[]
) => {
try {
return await fetcherFn({
options: { url, query, method },
// Transform the input array into an object
input: args.reduce((obj, val, i) => {
obj[input[i][0]!] = val
return obj
}, {}),
fetch: fetcherRef.current,
})
} catch (error) {
// SWR will not log errors, but any error that's not an instance
// of CommerceError is not welcomed by this hook
if (!(error instanceof CommerceError)) {
console.error(error)
}
throw error
}
}
const response = useSWR(
() => {
const opts = typeof options === 'function' ? options() : options
return opts
? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])]
: null
},
fetcher,
swrOptions
)
if (!('isLoading' in response)) {
defineProperty(response, 'isLoading', {
get() {
return response.data === undefined
},
enumerable: true,
})
}
return response
}
export default useData