Memoize functions in commerce hooks and debounce update

This commit is contained in:
Luis Alvarez 2020-10-08 20:36:31 -05:00
parent 0aac955910
commit 5c4f3e7ff2
10 changed files with 95 additions and 49 deletions

View File

@ -1,3 +1,4 @@
import { useCallback } from 'react'
import type { Fetcher } from '@lib/commerce'
import { default as useCartAddItem } from '@lib/commerce/cart/use-add-item'
import type { ItemBody, AddItemBody } from '../api/cart'
@ -21,11 +22,13 @@ function fetcher(fetch: Fetcher<Cart>, { item }: AddItemBody) {
export default function useAddItem() {
const { mutate } = useCart()
const fn = useCartAddItem<Cart, AddItemBody>(fetcher)
const addItem = async (input: UpdateItemInput) => {
const data = await fn({ item: input })
await mutate(data, false)
return data
}
return addItem
return useCallback(
async function addItem(input: UpdateItemInput) {
const data = await fn({ item: input })
await mutate(data, false)
return data
},
[fn, mutate]
)
}

View File

@ -1,3 +1,4 @@
import { useCallback } from 'react'
import type { Fetcher } from '@lib/commerce'
import { default as useCartRemoveItem } from '@lib/commerce/cart/use-remove-item'
import type { RemoveItemBody } from '../api/cart'
@ -21,11 +22,13 @@ export function fetcher(
export default function useRemoveItem(item?: any) {
const { mutate } = useCart()
const fn = useCartRemoveItem<Cart | null, RemoveItemBody>(fetcher)
const removeItem = async (input: RemoveItemInput) => {
const data = await fn({ itemId: input.id ?? item?.id })
await mutate(data, false)
return data
}
return removeItem
return useCallback(
async function removeItem(input: RemoveItemInput) {
const data = await fn({ itemId: input.id ?? item?.id })
await mutate(data, false)
return data
},
[fn, mutate]
)
}

View File

@ -1,3 +1,5 @@
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import type { Fetcher } from '@lib/commerce'
import { default as useCartUpdateItem } from '@lib/commerce/cart/use-update-item'
import type { ItemBody, UpdateItemBody } from '../api/cart'
@ -26,21 +28,23 @@ function fetcher(
})
}
export default function useUpdateItem(item?: any) {
export default function useUpdateItem(item?: any, cfg?: { wait?: number }) {
const { mutate } = useCart()
const fn = useCartUpdateItem<Cart | null, UpdateItemBody>(fetcher)
const updateItem = async (input: UpdateItemInput) => {
const data = await fn({
itemId: input.id ?? item?.id,
item: {
productId: input.productId ?? item?.product_id,
variantId: input.productId ?? item?.variant_id,
quantity: input.quantity,
},
})
await mutate(data, false)
return data
}
return updateItem
return useCallback(
debounce(async (input: UpdateItemInput) => {
const data = await fn({
itemId: input.id ?? item?.id,
item: {
productId: input.productId ?? item?.product_id,
variantId: input.productId ?? item?.variant_id,
quantity: input.quantity,
},
})
await mutate(data, false)
return data
}, cfg?.wait ?? 500),
[fn, mutate]
)
}

View File

@ -19,10 +19,9 @@ const CartProvider: FC<CartProviderProps> = ({ children, query, url }) => {
}
function useCart<C>() {
const { fetcher: fetch, cartCookie } = useCommerce()
const fetcher = (url?: string, query?: string) => {
return Cookies.get(cartCookie) ? fetch({ url, query }) : null
}
const { fetcherRef, cartCookie } = useCommerce()
const fetcher = (url?: string, query?: string) =>
Cookies.get(cartCookie) ? fetcherRef.current({ url, query }) : null
const { url, query } = useContext(CartContext)
const response = useSWR([url, query], fetcher, {
revalidateOnFocus: false,

View File

@ -1,11 +1,15 @@
import { useCallback } from 'react'
import { Fetcher, useCommerce } from '..'
export default function useAddItem<T, Input>(
fetcher: (fetch: Fetcher<T>, input: Input) => T | Promise<T>
) {
const { fetcher: fetch } = useCommerce()
const { fetcherRef } = useCommerce()
return async function addItem(input: Input) {
return fetcher(fetch, input)
}
return useCallback(
function addItem(input: Input) {
return fetcher(fetcherRef.current, input)
},
[fetcher]
)
}

View File

@ -1,11 +1,15 @@
import { useCallback } from 'react'
import { Fetcher, useCommerce } from '..'
export default function useRemoveItem<T, Input>(
fetcher: (fetch: Fetcher<T>, input: Input) => T | Promise<T>
) {
const { fetcher: fetch } = useCommerce()
const { fetcherRef } = useCommerce()
return async function removeItem(input: Input) {
return fetcher(fetch, input)
}
return useCallback(
function removeItem(input: Input) {
return fetcher(fetcherRef.current, input)
},
[fetcher]
)
}

View File

@ -1,11 +1,15 @@
import { useCallback } from 'react'
import { Fetcher, useCommerce } from '..'
export default function useUpdateItem<T, Input>(
fetcher: (fetch: Fetcher<T>, input: Input) => T | Promise<T>
) {
const { fetcher: fetch } = useCommerce()
const { fetcherRef } = useCommerce()
return async function updateItem(input: Input) {
return fetcher(fetch, input)
}
return useCallback(
function updateItem(input: Input) {
return fetcher(fetcherRef.current, input)
},
[fetcher]
)
}

View File

@ -1,14 +1,21 @@
import { createContext, ReactNode, useContext, useMemo } from 'react'
import {
ReactNode,
MutableRefObject,
createContext,
useContext,
useMemo,
useRef,
} from 'react'
const Commerce = createContext<CommerceConfig | null>(null)
export type CommerceProps = {
children?: ReactNode
config: CommerceConfig
config: { fetcher: Fetcher<any> } & CommerceConfig
}
export type CommerceConfig = {
fetcher: Fetcher<any>
fetcherRef: MutableRefObject<any>
locale: string
cartCookie: string
}
@ -28,17 +35,16 @@ export function CommerceProvider({ children, config }: CommerceProps) {
throw new Error('CommerceProvider requires a valid config object')
}
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(
() => ({
fetcher: config.fetcher,
fetcherRef,
locale: config.locale,
cartCookie: config.cartCookie,
}),
// Even though the fetcher is a function, it's never expected to be
// added dynamically (We should say that on the docs for this hook)
[config.fetcher, config.locale, config.cartCookie]
[config.locale, config.cartCookie]
)
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>

View File

@ -25,6 +25,7 @@
"cookie": "^0.4.1",
"js-cookie": "^2.2.1",
"lodash": "^4.17.20",
"lodash.debounce": "^4.0.8",
"next": "^9.5.4-canary.23",
"postcss-nesting": "^7.0.1",
"react": "^16.13.1",
@ -39,6 +40,7 @@
"@graphql-codegen/typescript-operations": "^1.17.8",
"@types/cookie": "^0.4.0",
"@types/js-cookie": "^2.2.6",
"@types/lodash.debounce": "^4.0.6",
"@types/node": "^14.11.2",
"@types/react": "^16.9.49",
"graphql": "^15.3.0",

View File

@ -1572,6 +1572,18 @@
dependencies:
"@types/node" "*"
"@types/lodash.debounce@^4.0.6":
version "4.0.6"
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz#c5a2326cd3efc46566c47e4c0aa248dc0ee57d60"
integrity sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.161"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18"
integrity sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA==
"@types/node@*", "@types/node@^14.11.2":
version "14.11.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"
@ -4703,6 +4715,11 @@ lodash._reinterpolate@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"