Setting up the right data hooks

This commit is contained in:
Belen Curcio 2020-10-29 12:51:53 -03:00
parent 1e61afbbea
commit e1d90b3ee0
30 changed files with 248 additions and 21 deletions

View File

@ -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'

View 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>
)
}

View 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
}
}

View 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>
}

View 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
}

View 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

View File

@ -0,0 +1 @@
export { default as useCart } from './hooks/useCart'

View File

@ -0,0 +1,7 @@
const bigcommerce = {
cart: {
url: '/api/bigcommerce/cart',
},
}
export { bigcommerce }

View 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][]

View File

@ -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 {

View File

@ -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'

View File

@ -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
}

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -1 +0,0 @@
export { default as useCart } from './useCart'

View File

View File

@ -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": {

View File

@ -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,

View File

@ -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}</>

View File

@ -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({

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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({

View File

@ -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'

View File

@ -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'

View File

@ -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"