mirror of
https://github.com/vercel/commerce.git
synced 2025-06-29 09:51:22 +00:00
Setting up the right data hooks
This commit is contained in:
parent
1e61afbbea
commit
e1d90b3ee0
@ -1,6 +1,6 @@
|
||||
import { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { UserNav } from '@components/core'
|
||||
import { UserNav } from '@components/common'
|
||||
import { Button } from '@components/ui'
|
||||
import { Bag, Cross, Check } from '@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
|
41
components/commerce/context.tsx
Normal file
41
components/commerce/context.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { ReactNode, FC, useRef, useMemo, MutableRefObject } from 'react'
|
||||
import { Fetcher } from './types'
|
||||
|
||||
export interface State {
|
||||
fetcherRef: MutableRefObject<Fetcher<any>>
|
||||
locale: string
|
||||
cartCookie: string
|
||||
}
|
||||
|
||||
export type CommerceProps = {
|
||||
children: ReactNode
|
||||
config: CommerceConfig
|
||||
}
|
||||
|
||||
export type CommerceConfig = { fetcher: Fetcher<any> } & Omit<
|
||||
State,
|
||||
'fetcherRef'
|
||||
>
|
||||
|
||||
const initialState = {}
|
||||
|
||||
export const CommerceContext = React.createContext<State | any>(initialState)
|
||||
CommerceContext.displayName = 'CommerceContext'
|
||||
|
||||
export const CommerceProvider: FC<CommerceProps> = ({ children, config }) => {
|
||||
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(
|
||||
() => ({
|
||||
fetcherRef,
|
||||
locale: config.locale,
|
||||
cartCookie: config.cartCookie,
|
||||
}),
|
||||
[config.locale, config.cartCookie]
|
||||
)
|
||||
|
||||
return (
|
||||
<CommerceContext.Provider value={cfg}>{children}</CommerceContext.Provider>
|
||||
)
|
||||
}
|
40
components/commerce/errors.ts
Normal file
40
components/commerce/errors.ts
Normal file
@ -0,0 +1,40 @@
|
||||
export type ErrorData = {
|
||||
message: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
export type ErrorProps = {
|
||||
code?: string
|
||||
} & (
|
||||
| { message: string; errors?: never }
|
||||
| { message?: never; errors: ErrorData[] }
|
||||
)
|
||||
|
||||
export class CommerceError extends Error {
|
||||
code?: string
|
||||
errors: ErrorData[]
|
||||
|
||||
constructor({ message, code, errors }: ErrorProps) {
|
||||
const error: ErrorData = message
|
||||
? { message, ...(code ? { code } : {}) }
|
||||
: errors![0]
|
||||
|
||||
super(error.message)
|
||||
this.errors = message ? [error] : errors!
|
||||
|
||||
if (error.code) this.code = error.code
|
||||
}
|
||||
}
|
||||
|
||||
export class FetcherError extends CommerceError {
|
||||
status: number
|
||||
|
||||
constructor(
|
||||
options: {
|
||||
status: number
|
||||
} & ErrorProps
|
||||
) {
|
||||
super(options)
|
||||
this.status = options.status
|
||||
}
|
||||
}
|
31
components/commerce/hooks/useCart.ts
Normal file
31
components/commerce/hooks/useCart.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { responseInterface } from 'swr'
|
||||
import Cookies from 'js-cookie'
|
||||
import type { HookInput, HookFetcher, HookFetcherOptions } from '../types'
|
||||
import useData, { SwrOptions } from './useData'
|
||||
import { useCommerce } from './useCommerce'
|
||||
|
||||
export type CartResponse<Result> = responseInterface<Result, Error> & {
|
||||
isEmpty: boolean
|
||||
}
|
||||
|
||||
export type CartInput = {
|
||||
cartId: string | undefined
|
||||
}
|
||||
|
||||
export default function useCart<Result>(
|
||||
options: HookFetcherOptions,
|
||||
input: HookInput,
|
||||
fetcherFn: HookFetcher<Result, CartInput>,
|
||||
swrOptions?: SwrOptions<Result, CartInput>
|
||||
) {
|
||||
const { 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 Object.assign(response, { isEmpty: true }) as CartResponse<Result>
|
||||
}
|
10
components/commerce/hooks/useCommerce.ts
Normal file
10
components/commerce/hooks/useCommerce.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react'
|
||||
import { CommerceContext } from '../context'
|
||||
|
||||
export const useCommerce = () => {
|
||||
const context = useContext(CommerceContext)
|
||||
if (context === undefined) {
|
||||
throw new Error(`useCommerce must be used within a CommerceProvider`)
|
||||
}
|
||||
return context
|
||||
}
|
60
components/commerce/hooks/useData.ts
Normal file
60
components/commerce/hooks/useData.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import useSWR, { ConfigInterface, responseInterface } from 'swr'
|
||||
import type { HookInput, HookFetcher, HookFetcherOptions } from '../types'
|
||||
import { CommerceError } from '../errors'
|
||||
import { useCommerce } from './useCommerce'
|
||||
|
||||
export type SwrOptions<Result, Input = null> = ConfigInterface<
|
||||
Result,
|
||||
CommerceError,
|
||||
HookFetcher<Result, Input>
|
||||
>
|
||||
|
||||
export type UseData = <Result = any, Input = null>(
|
||||
options: HookFetcherOptions | (() => HookFetcherOptions | null),
|
||||
input: HookInput,
|
||||
fetcherFn: HookFetcher<Result, Input>,
|
||||
swrOptions?: SwrOptions<Result, Input>
|
||||
) => responseInterface<Result, CommerceError>
|
||||
|
||||
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(
|
||||
{ url, query, method },
|
||||
// Transform the input array into an object
|
||||
args.reduce((obj, val, i) => {
|
||||
obj[input[i][0]!] = val
|
||||
return obj
|
||||
}, {}),
|
||||
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
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export default useData
|
1
components/commerce/index.ts
Normal file
1
components/commerce/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as useCart } from './hooks/useCart'
|
7
components/commerce/providersMap.ts
Normal file
7
components/commerce/providersMap.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const bigcommerce = {
|
||||
cart: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
},
|
||||
}
|
||||
|
||||
export { bigcommerce }
|
23
components/commerce/types.ts
Normal file
23
components/commerce/types.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export type Fetcher<T> = (options: FetcherOptions) => T | Promise<T>
|
||||
|
||||
export type FetcherOptions = {
|
||||
url?: string
|
||||
query?: string
|
||||
method?: string
|
||||
variables?: any
|
||||
body?: any
|
||||
}
|
||||
|
||||
export type HookFetcher<Result, Input = null> = (
|
||||
options: HookFetcherOptions | null,
|
||||
input: Input,
|
||||
fetch: <T = Result>(options: FetcherOptions) => Promise<T>
|
||||
) => Result | Promise<Result>
|
||||
|
||||
export type HookFetcherOptions = {
|
||||
query?: string
|
||||
url?: string
|
||||
method?: string
|
||||
}
|
||||
|
||||
export type HookInput = [string, string | number | boolean | undefined][]
|
@ -6,7 +6,7 @@ import type { Page } from '@bigcommerce/storefront-data-hooks/api/operations/get
|
||||
import getSlug from '@lib/get-slug'
|
||||
import { Github } from '@components/icons'
|
||||
import { Logo, Container } from '@components/ui'
|
||||
import { I18nWidget } from '@components/core'
|
||||
import { I18nWidget } from '@components/common'
|
||||
import s from './Footer.module.css'
|
||||
|
||||
interface Props {
|
||||
|
@ -5,7 +5,7 @@ import type { Page } from '@bigcommerce/storefront-data-hooks/api/operations/get
|
||||
import { CommerceProvider } from '@bigcommerce/storefront-data-hooks'
|
||||
import { CartSidebarView } from '@components/cart'
|
||||
import { Container, Sidebar, Button, Modal, Toast } from '@components/ui'
|
||||
import { Navbar, Featurebar, Footer } from '@components/core'
|
||||
import { Navbar, Featurebar, Footer } from '@components/common'
|
||||
import { LoginView, SignUpView, ForgotPassword } from '@components/auth'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { usePreventScroll } from '@react-aria/overlays'
|
||||
|
@ -2,7 +2,7 @@ import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import s from './Navbar.module.css'
|
||||
import { Logo } from '@components/ui'
|
||||
import { Searchbar, UserNav } from '@components/core'
|
||||
import { Searchbar, UserNav } from '@components/common'
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
|
||||
import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer'
|
||||
import { Menu } from '@headlessui/react'
|
||||
import { Heart, Bag } from '@components/icons'
|
||||
import { Avatar } from '@components/core'
|
||||
import { Avatar } from '@components/common'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import DropdownMenu from './DropdownMenu'
|
||||
import s from './UserNav.module.css'
|
||||
|
@ -3,7 +3,7 @@ import cn from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import type { ProductNode } from '@bigcommerce/storefront-data-hooks/api/operations/get-all-products'
|
||||
import usePrice from '@bigcommerce/storefront-data-hooks/use-price'
|
||||
import { EnhancedImage } from '@components/core'
|
||||
import { EnhancedImage } from '@components/common'
|
||||
import s from './ProductCard.module.css'
|
||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||
|
||||
|
@ -7,7 +7,7 @@ import s from './ProductView.module.css'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { Swatch, ProductSlider } from '@components/product'
|
||||
import { Button, Container } from '@components/ui'
|
||||
import { HTMLContent } from '@components/core'
|
||||
import { HTMLContent } from '@components/common'
|
||||
|
||||
import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item'
|
||||
import type { ProductNode } from '@bigcommerce/storefront-data-hooks/api/operations/get-product'
|
||||
|
@ -8,7 +8,7 @@ import useRemoveItem from '@bigcommerce/storefront-data-hooks/wishlist/use-remov
|
||||
import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { Button } from '@components/ui'
|
||||
import { HTMLContent } from '@components/core'
|
||||
import { HTMLContent } from '@components/common'
|
||||
import { Trash } from '@components/icons'
|
||||
import s from './WishlistCard.module.css'
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
export { default as useCart } from './useCart'
|
@ -17,11 +17,13 @@
|
||||
"@next/bundle-analyzer": "^9.5.5",
|
||||
"@react-aria/overlays": "^3.4.0",
|
||||
"@tailwindcss/ui": "^0.6.2",
|
||||
"@types/js-cookie": "^2.2.6",
|
||||
"@vercel/fetch": "^6.1.0",
|
||||
"bowser": "^2.11.0",
|
||||
"classnames": "^2.2.6",
|
||||
"email-validator": "^2.0.4",
|
||||
"intersection-observer": "^0.11.0",
|
||||
"js-cookie": "^2.2.1",
|
||||
"keen-slider": "^5.2.4",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.random": "^3.2.0",
|
||||
@ -36,6 +38,7 @@
|
||||
"react-intersection-observer": "^8.29.1",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
"react-ticker": "^1.2.2",
|
||||
"swr": "^0.3.7",
|
||||
"tailwindcss": "^1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -3,7 +3,7 @@ import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
||||
import getPage from '@bigcommerce/storefront-data-hooks/api/operations/get-page'
|
||||
import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||
import getSlug from '@lib/get-slug'
|
||||
import { Layout, HTMLContent } from '@components/core'
|
||||
import { Layout, HTMLContent } from '@components/common'
|
||||
|
||||
export async function getStaticProps({
|
||||
preview,
|
||||
|
@ -5,7 +5,7 @@ import { FC } from 'react'
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
import { ManagedUIContext } from '@components/ui/context'
|
||||
import { Head } from '@components/core'
|
||||
import { Head } from '@components/common'
|
||||
|
||||
const Noop: FC = ({ children }) => <>{children}</>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { GetStaticPropsContext } from 'next'
|
||||
import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
||||
import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||
import { Layout } from '@components/core'
|
||||
import { Layout } from '@components/common'
|
||||
import { Container } from '@components/ui'
|
||||
|
||||
export async function getStaticProps({
|
||||
|
@ -3,7 +3,7 @@ import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
||||
import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||
import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
|
||||
import usePrice from '@bigcommerce/storefront-data-hooks/use-price'
|
||||
import { Layout } from '@components/core'
|
||||
import { Layout } from '@components/common'
|
||||
import { Button } from '@components/ui'
|
||||
import { Bag, Cross, Check } from '@components/icons'
|
||||
import { CartItem } from '@components/cart'
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
||||
import rangeMap from '@lib/range-map'
|
||||
import { Layout } from '@components/core'
|
||||
import { Layout } from '@components/common'
|
||||
import { Grid, Marquee, Hero } from '@components/ui'
|
||||
import { ProductCard } from '@components/product'
|
||||
import HomeAllProductsGrid from '@components/core/HomeAllProductsGrid'
|
||||
import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid'
|
||||
|
||||
import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
||||
import getAllProducts from '@bigcommerce/storefront-data-hooks/api/operations/get-all-products'
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { GetStaticPropsContext } from 'next'
|
||||
import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
||||
import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||
import { Layout } from '@components/core'
|
||||
import { Layout } from '@components/common'
|
||||
import { Container, Text } from '@components/ui'
|
||||
import { Bag } from '@components/icons'
|
||||
|
||||
|
@ -4,7 +4,7 @@ import type {
|
||||
InferGetStaticPropsType,
|
||||
} from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Layout } from '@components/core'
|
||||
import { Layout } from '@components/common'
|
||||
import { ProductView } from '@components/product'
|
||||
|
||||
// Data
|
||||
|
@ -2,7 +2,7 @@ import type { GetStaticPropsContext } from 'next'
|
||||
import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
||||
import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||
import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer'
|
||||
import { Layout } from '@components/core'
|
||||
import { Layout } from '@components/common'
|
||||
import { Container, Text } from '@components/ui'
|
||||
|
||||
export async function getStaticProps({
|
||||
|
@ -6,7 +6,7 @@ import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
||||
import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||
import getSiteInfo from '@bigcommerce/storefront-data-hooks/api/operations/get-site-info'
|
||||
import useSearch from '@bigcommerce/storefront-data-hooks/products/use-search'
|
||||
import { Layout } from '@components/core'
|
||||
import { Layout } from '@components/common'
|
||||
import { ProductCard } from '@components/product'
|
||||
import { Container, Grid, Skeleton } from '@components/ui'
|
||||
|
||||
|
@ -2,7 +2,7 @@ import type { GetStaticPropsContext } from 'next'
|
||||
import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
||||
import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||
import useWishlist from '@bigcommerce/storefront-data-hooks/wishlist/use-wishlist'
|
||||
import { Layout } from '@components/core'
|
||||
import { Layout } from '@components/common'
|
||||
import { Heart } from '@components/icons'
|
||||
import { Container, Text } from '@components/ui'
|
||||
import { WishlistCard } from '@components/wishlist'
|
||||
|
14
yarn.lock
14
yarn.lock
@ -1815,6 +1815,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
|
||||
integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==
|
||||
|
||||
"@types/js-cookie@^2.2.6":
|
||||
version "2.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f"
|
||||
integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==
|
||||
|
||||
"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6":
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
|
||||
@ -3884,7 +3889,7 @@ jest-worker@^26.6.1:
|
||||
merge-stream "^2.0.0"
|
||||
supports-color "^7.0.0"
|
||||
|
||||
js-cookie@2.2.1:
|
||||
js-cookie@2.2.1, js-cookie@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
|
||||
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
|
||||
@ -5850,6 +5855,13 @@ swr@0.3.6:
|
||||
dependencies:
|
||||
dequal "2.0.2"
|
||||
|
||||
swr@^0.3.7:
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/swr/-/swr-0.3.7.tgz#331c704b135fd0f320f48a7355e11e624efb5cd0"
|
||||
integrity sha512-/w7ZFBRpyWZJTOfl1OPsf93goeBPnfBXgJ7BBqqRGNidynqTO2sqinlLbXBbTNcNdoJwNJLVEjn2aDHJh5p1Kw==
|
||||
dependencies:
|
||||
dequal "2.0.2"
|
||||
|
||||
tailwindcss@^1.9:
|
||||
version "1.9.6"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.9.6.tgz#0c5089911d24e1e98e592a31bfdb3d8f34ecf1a0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user