Add loading state for data hooks

This commit is contained in:
Luis Alvarez 2021-01-19 17:40:27 -05:00
parent 4a268e738d
commit 10318b2fab
6 changed files with 101 additions and 52 deletions

View File

@ -1,10 +1,9 @@
import { normalizeCart } from '../lib/normalize'
import type { HookFetcher } from '@commerce/utils/types'
import type { SwrOptions } from '@commerce/utils/use-data'
import useCommerceCart, { CartInput } from '@commerce/cart/use-cart'
import type { Cart as BigCommerceCart } from '../api/cart'
import update from "@framework/lib/immutability"
import update from '@framework/lib/immutability'
const defaultOpts = {
url: '/api/bigcommerce/cart',
@ -13,7 +12,7 @@ const defaultOpts = {
type UseCartResponse = BigCommerceCart & Cart
export const fetcher: HookFetcher<UseCartResponse | null , CartInput> = (
export const fetcher: HookFetcher<UseCartResponse | null, CartInput> = (
options,
{ cartId },
fetch
@ -42,11 +41,11 @@ export function extendHook(
set: (x) => x,
})
return response.data ? update(response, {
data: { $set: normalizeCart(response.data ) }
}) : response
return response.data
? update(response, {
data: { $set: normalizeCart(response.data) },
})
: response
}
useCart.extend = extendHook

View File

@ -1,14 +1,13 @@
import update from "@framework/lib/immutability"
import { Cart, CartItem, Product } from '../../types'
import update from '@framework/lib/immutability'
function normalizeProductOption(productOption:any) {
function normalizeProductOption(productOption: any) {
const {
node: {
entityId,
values: { edges },
...rest
},
} = productOption;
} = productOption
return {
id: entityId,
@ -30,52 +29,52 @@ export function normalizeProduct(productNode: any): Product {
return update(productNode, {
id: { $set: String(id) },
images: {
$apply: ({edges} : any) => edges?.map(
({ node: { urlOriginal, altText, ...rest } }: any) => ({
$apply: ({ edges }: any) =>
edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
url: urlOriginal,
alt: altText,
...rest,
})
)
},
})),
},
variants: {
$apply: ({edges} : any) => edges?.map(
({ node: { entityId, productOptions, ...rest } }: any) => ({
$apply: ({ edges }: any) =>
edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({
id: entityId,
options: productOptions?.edges
? productOptions.edges.map(normalizeProductOption)
: [],
...rest,
})
)
})),
},
options: {
$set: productOptions.edges ? productOptions?.edges.map(normalizeProductOption) : []
$set: productOptions.edges
? productOptions?.edges.map(normalizeProductOption)
: [],
},
brand: {
$apply:(brand : any) => brand?.entityId ? brand?.entityId : null,
},
slug: {
$set: path?.replace(/^\/+|\/+$/g, '')
$apply: (brand: any) => (brand?.entityId ? brand?.entityId : null),
},
slug: {
$set: path?.replace(/^\/+|\/+$/g, ''),
},
price: {
$set: {
value: prices?.price.value,
currencyCode: prices?.price.currencyCode,
}
},
$unset: ['entityId']
},
},
$unset: ['entityId'],
})
}
export function normalizeCart(data: any): Cart {
return update(data, {
$auto: {
items: { $set: data?.line_items?.physical_items?.map(itemsToProducts)},
items: { $set: data?.line_items?.physical_items?.map(itemsToProducts) },
subTotal: { $set: data?.base_amount },
total: { $set: data?.cart_amount }
total: { $set: data?.cart_amount },
},
$unset: ['created_time', 'coupons', 'line_items', 'email']
$unset: ['created_time', 'coupons', 'line_items', 'email'],
})
}
@ -92,24 +91,28 @@ function itemsToProducts(item: any): CartItem {
extended_list_price,
extended_sale_price,
...rest
} = item;
} = item
return update(item, {
$auto: {
prices: {
$auto: {
listPrice: { $set: list_price },
salePrice: { $set: sale_price } ,
salePrice: { $set: sale_price },
extendedListPrice: { $set: extended_list_price },
extendedSalePrice: { $set: extended_sale_price },
}
},
},
images: {
$set: [
{
alt: name,
url: image_url,
},
],
},
images: { $set: [{
alt: name,
url: image_url
}]},
productId: { $set: product_id },
variantId: { $set: variant_id }
}
variantId: { $set: variant_id },
},
})
}

View File

@ -1,12 +1,10 @@
import type { responseInterface } from 'swr'
import Cookies from 'js-cookie'
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
import useData, { SwrOptions } from '../utils/use-data'
import useData, { ResponseState, SwrOptions } from '../utils/use-data'
import { useCommerce } from '..'
export type CartResponse<Result> = responseInterface<Result, Error> & {
isEmpty: boolean
}
export type CartResponse<Result> = ResponseState<Result> & { isEmpty: boolean }
export type CartInput = {
cartId: Cart['id']
@ -17,7 +15,7 @@ export default function useCart<Result>(
input: HookInput,
fetcherFn: HookFetcher<Result, CartInput>,
swrOptions?: SwrOptions<Result, CartInput>
) {
): CartResponse<Result> {
const { cartCookie } = useCommerce()
const fetcher: typeof fetcherFn = (options, input, fetch) => {
@ -27,5 +25,5 @@ export default function useCart<Result>(
const response = useData(options, input, fetcher, swrOptions)
return Object.assign(response, { isEmpty: true }) as CartResponse<Result>
return Object.assign(response, { isEmpty: true })
}

View File

@ -0,0 +1,37 @@
// Taken from https://fettblog.eu/typescript-assertion-signatures/
type InferValue<Prop extends PropertyKey, Desc> = Desc extends {
get(): any
value: any
}
? never
: Desc extends { value: infer T }
? Record<Prop, T>
: Desc extends { get(): infer T }
? Record<Prop, T>
: never
type DefineProperty<
Prop extends PropertyKey,
Desc extends PropertyDescriptor
> = Desc extends { writable: any; set(val: any): any }
? never
: Desc extends { writable: any; get(): any }
? never
: Desc extends { writable: false }
? Readonly<InferValue<Prop, Desc>>
: Desc extends { writable: true }
? InferValue<Prop, Desc>
: Readonly<InferValue<Prop, Desc>>
export default function defineProperty<
Obj extends object,
Key extends PropertyKey,
PDesc extends PropertyDescriptor
>(
obj: Obj,
prop: Key,
val: PDesc
): asserts obj is Obj & DefineProperty<Key, PDesc> {
Object.defineProperty(obj, prop, val)
}

View File

@ -1,5 +1,6 @@
import useSWR, { ConfigInterface, responseInterface } from 'swr'
import type { HookInput, HookFetcher, HookFetcherOptions } from './types'
import defineProperty from './define-property'
import { CommerceError } from './errors'
import { useCommerce } from '..'
@ -9,12 +10,16 @@ export type SwrOptions<Result, Input = null> = ConfigInterface<
HookFetcher<Result, Input>
>
export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
isLoading: boolean
}
export type UseData = <Result = any, Input = null>(
options: HookFetcherOptions | (() => HookFetcherOptions | null),
input: HookInput,
fetcherFn: HookFetcher<Result, Input>,
swrOptions?: SwrOptions<Result, Input>
) => responseInterface<Result, CommerceError>
) => ResponseState<Result>
const useData: UseData = (options, input, fetcherFn, swrOptions) => {
const { fetcherRef } = useCommerce()
@ -54,6 +59,13 @@ const useData: UseData = (options, input, fetcherFn, swrOptions) => {
swrOptions
)
defineProperty(response, 'isLoading', {
get() {
return response.data === undefined
},
set: (x) => x,
})
return response
}

View File

@ -1,8 +1,8 @@
import type { responseInterface } from 'swr'
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
import useData, { SwrOptions } from '../utils/use-data'
import useData, { ResponseState, SwrOptions } from '../utils/use-data'
export type WishlistResponse<Result> = responseInterface<Result, Error> & {
export type WishlistResponse<Result> = ResponseState<Result> & {
isEmpty: boolean
}
@ -11,7 +11,7 @@ export default function useWishlist<Result, Input = null>(
input: HookInput,
fetcherFn: HookFetcher<Result, Input>,
swrOptions?: SwrOptions<Result, Input>
) {
): WishlistResponse<Result> {
const response = useData(options, input, fetcherFn, swrOptions)
return Object.assign(response, { isEmpty: true }) as WishlistResponse<Result>
return Object.assign(response, { isEmpty: true })
}