Skip to main content

Applying changes

Checkout UI extensions can use checkout APIs to make changes, such as updating cart lines, cart metafields, order notes, and privacy consent. Apply each change only after the buyer's intent is clear and when the new value is different. When possible, apply all changes at the same time. Before calling mutation APIs, check cart instructions to confirm that the change is allowed.

Rate limits may apply

Excessive changes may rate limit the extension and prevent it from applying more changes to the checkout. Rate-limited extensions can't block the buyer journey.


Anchor to Debounce user-driven changesDebounce user-driven changes

Debounce API calls that respond to user input. Calling an API after every keystroke or interaction creates unnecessary work and can degrade performance.

Debouncing input changes

import '@shopify/ui-extensions/preact';
import {render} from 'preact';
import {
useCallback,
useRef,
useState,
} from 'preact/hooks';

function Extension() {
const [value, setValue] = useState('');
const timerRef = useRef(null);

const handleInput = useCallback((event) => {
const newValue = event.currentTarget.value;
setValue(newValue);

// Clear any pending update.
if (timerRef.current) {
clearTimeout(timerRef.current);
}

// Wait for the buyer to stop typing before applying the change.
timerRef.current = setTimeout(() => {
if (
!shopify.instructions.value.metafields
.canSetCartMetafields
) {
return;
}

void shopify.applyMetafieldChange({
type: 'updateCartMetafield',
metafield: {
namespace: '$app:preferences',
key: 'gift-note',
type: 'single_line_text_field',
value: newValue,
},
});
}, 500);
}, []);

return (
<s-text-field
label="Gift note"
value={value}
onInput={handleInput}
/>
);
}

export default function extension() {
render(<Extension />, document.body);
}

Anchor to Avoid changes in render loopsAvoid changes in render loops

In code that runs on every render or in response to reactive state changes, don't apply changes to checkout state without appropriate guards. If a buyer action changes extension state, then compare the new value with the last value that your extension requested before calling the API.

Avoiding render-loop changes

import '@shopify/ui-extensions/preact';
import {render} from 'preact';
import {
useCallback,
useRef,
useState,
} from 'preact/hooks';

function Extension() {
const [includeGiftWrap, setIncludeGiftWrap] =
useState(false);
const lastRequestedValue = useRef('false');
const canSetCartMetafields =
shopify.instructions.value.metafields
.canSetCartMetafields;

const handleGiftWrapChange = useCallback((event) => {
const checked = event.currentTarget.checked;
const value = checked ? 'true' : 'false';
setIncludeGiftWrap(checked);

if (lastRequestedValue.current === value) {
return;
}

lastRequestedValue.current = value;
void shopify.applyMetafieldChange({
type: 'updateCartMetafield',
metafield: {
namespace: '$app:preferences',
key: 'include-gift-wrap',
type: 'boolean',
value,
},
});
}, []);

// ❌ Bad: runs during render when `includeGiftWrap` is true.
// if (includeGiftWrap) {
// shopify.applyMetafieldChange({...});
// }

if (!canSetCartMetafields) {
return null;
}

return (
<s-checkbox
label="Gift wrap"
checked={includeGiftWrap}
onChange={handleGiftWrapChange}
/>
);
}

export default function extension() {
render(<Extension />, document.body);
}

Anchor to React to state changes selectivelyReact to state changes selectively

When your extension reads checkout state, such as the shipping address or cart lines, apply changes only when the specific data your extension needs has changed. Don't apply a change for every state update. The following example synchronizes the country and province as one value, and the guard prevents another request when that value stays the same.

Reacting selectively to state changes

import '@shopify/ui-extensions/preact';
import {render} from 'preact';
import {useCallback, useEffect, useRef} from 'preact/hooks';

function Extension() {
const lastRegionRef = useRef(null);
const shippingAddress = shopify.shippingAddress?.value;
const countryCode = shippingAddress?.countryCode;
const provinceCode = shippingAddress?.provinceCode;
const region = countryCode
? [countryCode, provinceCode].filter(Boolean).join('-')
: null;
const canSetCartMetafields =
shopify.instructions.value.metafields
.canSetCartMetafields;

const syncShippingRegion = useCallback((newRegion) => {
return shopify.applyMetafieldChange({
type: 'updateCartMetafield',
metafield: {
namespace: '$app:preferences',
key: 'shipping-region',
type: 'single_line_text_field',
value: newRegion,
},
});
}, []);

useEffect(() => {
if (
!region ||
region === lastRegionRef.current ||
!canSetCartMetafields
) {
return;
}

async function syncRegion() {
const result = await syncShippingRegion(region);

if (result.type !== 'error') {
lastRegionRef.current = region;
}
}

void syncRegion();
}, [canSetCartMetafields, region, syncShippingRegion]);

return <s-text>Tracking shipping region</s-text>;
}

export default function extension() {
render(<Extension />, document.body);
}

Anchor to Apply changes at the same timeApply changes at the same time

If you need to make multiple changes, then apply them in parallel with Promise.all instead of awaiting each one sequentially. This reduces the time spent applying changes, avoids delays between calls, and makes the extension less likely to trip rate limits.

Synchronize all changes that can be applied from the same buyer intent. For example, use parallel calls when a buyer action updates a cart metafield and sets an order note.

Applying changes at the same time

import '@shopify/ui-extensions/preact';
import {render} from 'preact';

function Extension() {
const {metafields, notes} = shopify.instructions.value;

async function saveGiftPreferences() {
const [metafieldResult, noteResult] =
await Promise.all([
shopify.applyMetafieldChange({
type: 'updateCartMetafield',
metafield: {
namespace: '$app:preferences',
key: 'include-gift',
type: 'boolean',
value: 'true',
},
}),
shopify.applyNoteChange({
type: 'updateNote',
note: 'Please include a gift receipt.',
}),
]);

if (
metafieldResult.type === 'error' ||
noteResult.type === 'error'
) {
console.error('Gift preferences failed to save');
}
}

if (
!metafields.canSetCartMetafields ||
!notes.canUpdateNote
) {
return null;
}

return (
<s-button onClick={saveGiftPreferences}>
Save gift preferences
</s-button>
);
}

export default function extension() {
render(<Extension />, document.body);
}

Anchor to Handle errors gracefullyHandle errors gracefully

Don't retry aggressively. When a change returns an error, let the buyer retry manually. Consider retrying automatically only if the failure is due to a network error.


Was this page helpful?