Skip to content

Commit

Permalink
[CartProviderV2] Fix hydration errors (Shopify#2219)
Browse files Browse the repository at this point in the history
* Cart goes through localStorage only once

* Delay cart state changes until hydration finishes to avoid errors

* Expose `CartProviderV2` in beta

* Remove onLoad event check when using delayed state in CartProvider
  • Loading branch information
lordofthecactus authored Oct 6, 2022
1 parent bc80acd commit 34ffb8e
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 10 deletions.
12 changes: 12 additions & 0 deletions .changeset/many-kiwis-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@shopify/hydrogen': patch
---

Experimental version of a new cart provider is ready for beta testing.
`CartProviderV2` fixes race conditions with our current cart provider. After beta, `CartProviderV2` will become `CartProvider` requiring no code changes.

To try this new cart provider:

```
import {CartProviderV2} from '@shopify/hydrogen/experimental';
```
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
useTransition,
} from 'react';
import {CartFragmentFragment} from './graphql/CartFragment.js';
import {
AttributeInput,
Expand Down Expand Up @@ -226,14 +233,17 @@ export function CartProviderV2({
countryCode !== cartState?.context?.cart?.buyerIdentity?.countryCode &&
!cartState.context.errors;

const fetchingFromStorage = useRef(false);

/**
* Initializes cart with priority in this order:
* 1. cart props
* 2. localStorage cartId
*/
useEffect(() => {
if (!cartReady.current) {
if (!cartReady.current && !fetchingFromStorage.current) {
if (!cart && storageAvailable('localStorage')) {
fetchingFromStorage.current = true;
try {
const cartId = window.localStorage.getItem(CART_ID_STORAGE_KEY);
if (cartId) {
Expand Down Expand Up @@ -325,15 +335,19 @@ export function CartProviderV2({
[countryCode, customerAccessToken, onCartReadySend]
);

// Delays the cart state in the context if the page is hydrating
// preventing suspense boundary errors.
const cartDisplayState = useDelayedStateUntilHydration(cartState);

const cartContextValue = useMemo<CartWithActions>(() => {
return {
...(cartState?.context?.cart ?? {lines: [], attributes: []}),
status: transposeStatus(cartState.value),
error: cartState?.context?.errors,
totalQuantity: cartState?.context?.cart?.totalQuantity ?? 0,
...(cartDisplayState?.context?.cart ?? {lines: [], attributes: []}),
status: transposeStatus(cartDisplayState.value),
error: cartDisplayState?.context?.errors,
totalQuantity: cartDisplayState?.context?.cart?.totalQuantity ?? 0,
cartCreate,
linesAdd(lines: CartLineInput[]) {
if (cartState?.context?.cart?.id) {
if (cartDisplayState?.context?.cart?.id) {
onCartReadySend({
type: 'CARTLINE_ADD',
payload: {lines},
Expand Down Expand Up @@ -394,10 +408,10 @@ export function CartProviderV2({
};
}, [
cartCreate,
cartDisplayState?.context?.cart,
cartDisplayState?.context?.errors,
cartDisplayState.value,
cartFragment,
cartState?.context?.cart,
cartState?.context?.errors,
cartState.value,
onCartReadySend,
]);

Expand Down Expand Up @@ -434,6 +448,37 @@ function transposeStatus(
}
}

/**
* Delays a state update until hydration finishes. Useful for preventing suspense boundaries errors when updating a context
* @remarks this uses startTransition and waits for it to finish.
*/
function useDelayedStateUntilHydration<T>(state: T) {
const [isPending, startTransition] = useTransition();
const [delayedState, setDelayedState] = useState(state);

const firstTimePending = useRef(false);
if (isPending) {
firstTimePending.current = true;
}

const firstTimePendingFinished = useRef(false);
if (!isPending && firstTimePending.current) {
firstTimePendingFinished.current = true;
}

useEffect(() => {
startTransition(() => {
if (!firstTimePendingFinished.current) {
setDelayedState(state);
}
});
}, [state]);

const displayState = firstTimePendingFinished.current ? state : delayedState;

return displayState;
}

/** Check for storage availability funciton obtained from
* https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
*/
Expand Down

0 comments on commit 34ffb8e

Please sign in to comment.