-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make variant / image updates use useOptimistic
and startTransition
#1327
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import clsx from 'clsx'; | |
import { ProductOption, ProductVariant } from 'lib/shopify/types'; | ||
import { createUrl } from 'lib/utils'; | ||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'; | ||
import { useOptimistic, useTransition } from 'react'; | ||
|
||
type Combination = { | ||
id: string; | ||
|
@@ -21,14 +22,21 @@ export function VariantSelector({ | |
const router = useRouter(); | ||
const pathname = usePathname(); | ||
const searchParams = useSearchParams(); | ||
const [optimisticVariants, setOptimsticVariants] = useOptimistic(variants); | ||
const [optimisticOptions, setOptimisticOptions] = useOptimistic( | ||
new URLSearchParams(searchParams.toString()) | ||
); | ||
// eslint-disable-next-line no-unused-vars | ||
const [pending, startTransition] = useTransition(); | ||
|
||
const hasNoOptionsOrJustOneOption = | ||
!options.length || (options.length === 1 && options[0]?.values.length === 1); | ||
|
||
if (hasNoOptionsOrJustOneOption) { | ||
return null; | ||
} | ||
|
||
const combinations: Combination[] = variants.map((variant) => ({ | ||
const combinations: Combination[] = optimisticVariants.map((variant) => ({ | ||
id: variant.id, | ||
availableForSale: variant.availableForSale, | ||
// Adds key / value pairs for each variant (ie. "color": "Black" and "size": 'M"). | ||
|
@@ -45,14 +53,6 @@ export function VariantSelector({ | |
{option.values.map((value) => { | ||
const optionNameLowerCase = option.name.toLowerCase(); | ||
|
||
// Base option params on current params so we can preserve any other param state in the url. | ||
const optionSearchParams = new URLSearchParams(searchParams.toString()); | ||
|
||
// Update the option params using the current option to reflect how the url *would* change, | ||
// if the option was clicked. | ||
optionSearchParams.set(optionNameLowerCase, value); | ||
const optionUrl = createUrl(pathname, optionSearchParams); | ||
|
||
// In order to determine if an option is available for sale, we need to: | ||
// | ||
// 1. Filter out all other param state | ||
|
@@ -62,7 +62,7 @@ export function VariantSelector({ | |
// This is the "magic" that will cross check possible variant combinations and preemptively | ||
// disable combinations that are not available. For example, if the color gray is only available in size medium, | ||
// then all other sizes should be disabled. | ||
const filtered = Array.from(optionSearchParams.entries()).filter(([key, value]) => | ||
const filtered = Array.from(optimisticOptions.entries()).filter(([key, value]) => | ||
options.find( | ||
(option) => option.name.toLowerCase() === key && option.values.includes(value) | ||
) | ||
|
@@ -74,15 +74,36 @@ export function VariantSelector({ | |
); | ||
|
||
// The option is active if it's in the url params. | ||
const isActive = searchParams.get(optionNameLowerCase) === value; | ||
const isActive = optimisticOptions.get(optionNameLowerCase) === value; | ||
|
||
return ( | ||
<button | ||
key={value} | ||
aria-disabled={!isAvailableForSale} | ||
disabled={!isAvailableForSale} | ||
onClick={() => { | ||
router.replace(optionUrl, { scroll: false }); | ||
startTransition(() => { | ||
const newOptimisticVariants = optimisticVariants.map((variant) => { | ||
const updatedOptions = variant.selectedOptions.map((option) => { | ||
if (option.name.toLowerCase() === optionNameLowerCase) { | ||
return { ...option, value: value }; | ||
} | ||
return option; | ||
}); | ||
|
||
return { ...variant, selectedOptions: updatedOptions }; | ||
}); | ||
|
||
optimisticOptions.set(optionNameLowerCase, value); | ||
|
||
setOptimsticVariants(newOptimisticVariants); | ||
setOptimisticOptions(new URLSearchParams(optimisticOptions.toString())); | ||
|
||
const optionUrl = createUrl(pathname, optimisticOptions); | ||
|
||
// Navigate without page reload | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this comment's 100% accurate. It seems that the navigation can't complete without a full page render – at least in the sense that the search params are up-to-date, etc. While the UI is ready to interact with due to the useOptimistic hook, it's still waiting for the full page render before the search params update, so it's now open to race conditions for anything reading from the search params (eg, adding to cart) |
||
router.replace(optionUrl, { scroll: false }); | ||
}); | ||
}} | ||
title={`${option.name} ${value}${!isAvailableForSale ? ' (Out of Stock)' : ''}`} | ||
className={clsx( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit