@shopify/hydrogen-react
Version:
React components, hooks, and utilities for creating custom Shopify storefronts
1 lines • 17.4 kB
Source Map (JSON)
{"version":3,"file":"ProductProvider.mjs","sources":["../../src/ProductProvider.tsx"],"sourcesContent":["import {\n useMemo,\n useState,\n useEffect,\n useCallback,\n createContext,\n useContext,\n} from 'react';\nimport type {\n SelectedOption as SelectedOptionType,\n SellingPlan,\n SellingPlanAllocation,\n Product,\n ProductVariant as ProductVariantType,\n ProductVariantConnection,\n SellingPlan as SellingPlanType,\n SellingPlanAllocation as SellingPlanAllocationType,\n SellingPlanGroup as SellingPlanGroupType,\n SellingPlanGroupConnection,\n} from './storefront-api-types.js';\nimport type {PartialDeep} from 'type-fest';\nimport {flattenConnection} from './flatten-connection.js';\n\nconst ProductOptionsContext = createContext<ProductHookValue | null>(null);\n\ntype InitialVariantId = ProductVariantType['id'] | null;\n\ninterface ProductProviderProps {\n /** A [Product object](https://shopify.dev/api/storefront/reference/products/product). */\n data: PartialDeep<Product, {recurseIntoArrays: true}>;\n /** A `ReactNode` element. */\n children: React.ReactNode;\n /**\n * The initially selected variant.\n * The following logic applies to `initialVariantId`:\n * 1. If `initialVariantId` is provided, then it's used even if it's out of stock.\n * 2. If `initialVariantId` is provided but is `null`, then no variant is used.\n * 3. If nothing is passed to `initialVariantId` then the first available / in-stock variant is used.\n * 4. If nothing is passed to `initialVariantId` and no variants are in stock, then the first variant is used.\n */\n initialVariantId?: InitialVariantId;\n}\n\n/**\n * `<ProductProvider />` is a context provider that enables use of the `useProduct()` hook.\n *\n * It helps manage selected options and variants for a product.\n */\nexport function ProductProvider({\n children,\n data: product,\n initialVariantId: explicitVariantId,\n}: ProductProviderProps) {\n // The flattened variants\n const variants = useMemo(\n () => flattenConnection(product.variants ?? {}),\n [product.variants]\n );\n\n if (!isProductVariantArray(variants)) {\n throw new Error(\n `<ProductProvider/> requires 'product.variants.nodes' or 'product.variants.edges'`\n );\n }\n\n // All the options available for a product, based on all the variants\n const options = useMemo(() => getOptions(variants), [variants]);\n\n /**\n * Track the selectedVariant within the provider.\n */\n const [selectedVariant, setSelectedVariant] = useState<\n | PartialDeep<ProductVariantType, {recurseIntoArrays: true}>\n | undefined\n | null\n >(() => getVariantBasedOnIdProp(explicitVariantId, variants));\n\n /**\n * Track the selectedOptions within the provider. If a `initialVariantId`\n * is passed, use that to select initial options.\n */\n const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>(() =>\n getSelectedOptions(selectedVariant)\n );\n\n /**\n * When the initialVariantId changes, we need to make sure we\n * update the selected variant and selected options. If not,\n * then the selected variant and options will reference incorrect\n * values.\n */\n useEffect(() => {\n const newSelectedVariant = getVariantBasedOnIdProp(\n explicitVariantId,\n variants\n );\n setSelectedVariant(newSelectedVariant);\n setSelectedOptions(getSelectedOptions(newSelectedVariant));\n }, [explicitVariantId, variants]);\n\n /**\n * Allow the developer to select an option.\n */\n const setSelectedOption = useCallback(\n (name: string, value: string) => {\n setSelectedOptions((selectedOptions) => {\n const opts = {...selectedOptions, [name]: value};\n setSelectedVariant(getSelectedVariant(variants, opts));\n return opts;\n });\n },\n [setSelectedOptions, variants]\n );\n\n const isOptionInStock = useCallback(\n (option: string, value: string) => {\n const proposedVariant = getSelectedVariant(variants, {\n ...selectedOptions,\n ...{[option]: value},\n });\n\n return proposedVariant?.availableForSale ?? true;\n },\n [selectedOptions, variants]\n );\n\n const sellingPlanGroups = useMemo(\n () =>\n flattenConnection(product.sellingPlanGroups ?? {}).map(\n (sellingPlanGroup) => ({\n ...sellingPlanGroup,\n sellingPlans: flattenConnection(sellingPlanGroup?.sellingPlans ?? {}),\n })\n ),\n [product.sellingPlanGroups]\n );\n\n /**\n * Track the selectedSellingPlan within the hook. If `initialSellingPlanId`\n * is passed, use that as an initial value. Look it up from the `selectedVariant`, since\n * that is also a requirement.\n */\n const [selectedSellingPlan, setSelectedSellingPlan] = useState<\n PartialDeep<SellingPlan, {recurseIntoArrays: true}> | undefined\n >(undefined);\n\n const selectedSellingPlanAllocation = useMemo<\n PartialDeep<SellingPlanAllocation, {recurseIntoArrays: true}> | undefined\n >(() => {\n if (!selectedVariant || !selectedSellingPlan) {\n return;\n }\n\n if (\n !selectedVariant.sellingPlanAllocations?.nodes &&\n !selectedVariant.sellingPlanAllocations?.edges\n ) {\n throw new Error(\n `<ProductProvider/>: You must include 'sellingPlanAllocations.nodes' or 'sellingPlanAllocations.edges' in your variants in order to calculate selectedSellingPlanAllocation`\n );\n }\n\n return flattenConnection(selectedVariant.sellingPlanAllocations).find(\n (allocation) => allocation?.sellingPlan?.id === selectedSellingPlan.id\n );\n }, [selectedVariant, selectedSellingPlan]);\n\n const value = useMemo<ProductHookValue>(\n () => ({\n variants,\n variantsConnection: product.variants,\n options,\n selectedVariant,\n setSelectedVariant,\n selectedOptions,\n setSelectedOption,\n setSelectedOptions,\n isOptionInStock,\n selectedSellingPlan,\n setSelectedSellingPlan,\n selectedSellingPlanAllocation,\n sellingPlanGroups,\n sellingPlanGroupsConnection: product.sellingPlanGroups,\n }),\n [\n isOptionInStock,\n options,\n product.sellingPlanGroups,\n product.variants,\n selectedOptions,\n selectedSellingPlan,\n selectedSellingPlanAllocation,\n selectedVariant,\n sellingPlanGroups,\n setSelectedOption,\n variants,\n ]\n );\n\n return (\n <ProductOptionsContext.Provider value={value}>\n {children}\n </ProductOptionsContext.Provider>\n );\n}\n\n/**\n * Provides access to the context value provided by `<ProductProvider />`. Must be a descendent of `<ProductProvider />`.\n */\nexport function useProduct() {\n const context = useContext(ProductOptionsContext);\n\n if (!context) {\n throw new Error(`'useProduct' must be a child of <ProductProvider />`);\n }\n\n return context;\n}\n\nfunction getSelectedVariant(\n variants: PartialDeep<ProductVariantType, {recurseIntoArrays: true}>[],\n choices: SelectedOptions\n): PartialDeep<ProductVariantType, {recurseIntoArrays: true}> | undefined {\n /**\n * Ensure the user has selected all the required options, not just some.\n */\n if (\n !variants.length ||\n variants?.[0]?.selectedOptions?.length !== Object.keys(choices).length\n ) {\n return;\n }\n\n return variants?.find((variant) => {\n return Object.entries(choices).every(([name, value]) => {\n return variant?.selectedOptions?.some(\n (option) => option?.name === name && option?.value === value\n );\n });\n });\n}\n\nfunction getOptions(\n variants: PartialDeep<ProductVariantType, {recurseIntoArrays: true}>[]\n): OptionWithValues[] {\n const map = variants.reduce((memo, variant) => {\n if (!variant.selectedOptions) {\n throw new Error(`'getOptions' requires 'variant.selectedOptions'`);\n }\n variant?.selectedOptions?.forEach((opt) => {\n memo[opt?.name ?? ''] = memo[opt?.name ?? ''] || new Set();\n memo[opt?.name ?? ''].add(opt?.value ?? '');\n });\n\n return memo;\n }, {} as Record<string, Set<string>>);\n\n return Object.keys(map).map((option) => {\n return {\n name: option,\n values: Array.from(map[option]),\n };\n });\n}\n\nfunction getVariantBasedOnIdProp(\n explicitVariantId: InitialVariantId | undefined,\n variants: Array<\n PartialDeep<ProductVariantType, {recurseIntoArrays: true}> | undefined\n >\n) {\n // get the initial variant based on the logic outlined in the comments for 'initialVariantId' above\n // * 1. If `initialVariantId` is provided, then it's used even if it's out of stock.\n if (explicitVariantId) {\n const foundVariant = variants.find(\n (variant) => variant?.id === explicitVariantId\n );\n if (!foundVariant) {\n console.warn(\n `<ProductProvider/> received a 'initialVariantId' prop, but could not actually find a variant with that ID`\n );\n }\n return foundVariant;\n }\n // * 2. If `initialVariantId` is provided but is `null`, then no variant is used.\n if (explicitVariantId === null) {\n return null;\n }\n // * 3. If nothing is passed to `initialVariantId` then the first available / in-stock variant is used.\n // * 4. If nothing is passed to `initialVariantId` and no variants are in stock, then the first variant is used.\n if (explicitVariantId === undefined) {\n return variants.find((variant) => variant?.availableForSale) || variants[0];\n }\n}\n\nfunction getSelectedOptions(\n selectedVariant:\n | PartialDeep<ProductVariantType, {recurseIntoArrays: true}>\n | undefined\n | null\n): SelectedOptions {\n return selectedVariant?.selectedOptions\n ? selectedVariant.selectedOptions.reduce<SelectedOptions>(\n (memo, optionSet) => {\n memo[optionSet?.name ?? ''] = optionSet?.value ?? '';\n return memo;\n },\n {}\n )\n : {};\n}\n\nfunction isProductVariantArray(\n maybeVariantArray:\n | (PartialDeep<ProductVariantType, {recurseIntoArrays: true}> | undefined)[]\n | undefined\n): maybeVariantArray is PartialDeep<\n ProductVariantType,\n {recurseIntoArrays: true}\n>[] {\n if (!maybeVariantArray || !Array.isArray(maybeVariantArray)) {\n return false;\n }\n\n return true;\n}\n\nexport interface OptionWithValues {\n name: SelectedOptionType['name'];\n values: SelectedOptionType['value'][];\n}\n\ntype ProductHookValue = PartialDeep<\n {\n /** An array of the variant `nodes` from the `VariantConnection`. */\n variants: ProductVariantType[];\n variantsConnection?: ProductVariantConnection;\n /** An array of the product's options and values. */\n options: OptionWithValues[];\n /** The selected variant. */\n selectedVariant?: ProductVariantType | null;\n selectedOptions: SelectedOptions;\n /** The selected selling plan. */\n selectedSellingPlan?: SellingPlanType;\n /** The selected selling plan allocation. */\n selectedSellingPlanAllocation?: SellingPlanAllocationType;\n /** The selling plan groups. */\n sellingPlanGroups?: (Omit<SellingPlanGroupType, 'sellingPlans'> & {\n sellingPlans: SellingPlanType[];\n })[];\n sellingPlanGroupsConnection?: SellingPlanGroupConnection;\n },\n {recurseIntoArrays: true}\n> & {\n /** A callback to set the selected variant to the variant passed as an argument. */\n setSelectedVariant: (\n variant: PartialDeep<ProductVariantType, {recurseIntoArrays: true}> | null\n ) => void;\n /** A callback to set the selected option. */\n setSelectedOption: (\n name: SelectedOptionType['name'],\n value: SelectedOptionType['value']\n ) => void;\n /** A callback to set multiple selected options at once. */\n setSelectedOptions: (options: SelectedOptions) => void;\n /** A callback to set the selected selling plan to the one passed as an argument. */\n setSelectedSellingPlan: (\n sellingPlan: PartialDeep<SellingPlanType, {recurseIntoArrays: true}>\n ) => void;\n /** A callback that returns a boolean indicating if the option is in stock. */\n isOptionInStock: (\n name: SelectedOptionType['name'],\n value: SelectedOptionType['value']\n ) => boolean;\n};\n\nexport type SelectedOptions = {\n [key: string]: string;\n};\n"],"names":["ProductOptionsContext","createContext","ProductProvider","children","data","product","initialVariantId","explicitVariantId","variants","useMemo","flattenConnection","isProductVariantArray","Error","options","getOptions","selectedVariant","setSelectedVariant","useState","getVariantBasedOnIdProp","selectedOptions","setSelectedOptions","getSelectedOptions","useEffect","newSelectedVariant","setSelectedOption","useCallback","name","value","opts","getSelectedVariant","isOptionInStock","option","proposedVariant","availableForSale","sellingPlanGroups","map","sellingPlanGroup","sellingPlans","selectedSellingPlan","setSelectedSellingPlan","undefined","selectedSellingPlanAllocation","sellingPlanAllocations","nodes","edges","find","allocation","sellingPlan","id","variantsConnection","sellingPlanGroupsConnection","_jsx","useProduct","context","useContext","choices","length","Object","keys","variant","entries","every","some","reduce","memo","forEach","opt","Set","add","values","Array","from","foundVariant","console","warn","optionSet","maybeVariantArray","isArray"],"mappings":"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"}