Checkout UI Extension Integration Deep Dive - shopify-apac-ts/shopify-barebone-app-sample GitHub Wiki
This document describes about how Checkout UI Extensions work in browsers as Web Workers, which helps you to integrate them effectively and avoid pitfalls or unexpected issues.
- Shopify developer site resources
- Remote UI repository (used by the extension internally)
- Worker in Web Worker API (postMessage() is used by Remote UI above)
- This sample app's code
The following demo reads metafields and accepts discount codes in cart attributes and checkout UI both to share the value with each other. You may notice that a console.log for Extension() / useSettings
at the beginning of the extension code gets output multiple times and output again if the buyer changes the discount code in the checkout UI.
Especially, metafields need attention because their reference API (React hook) doesn't return values in the first loadings (you can check it with Extension() / appMetafield
console outputs from the code which shows useAppMetafields hooks return empty values in the first some calls).
Checkout_attr_discount_1.mp4
Demo 1: How the extension gets loaded multiple times and reactively
Also, the next demo shows the number of the console outputs differs based on the extension settings that enables / disables reading the metafields, cart attributes, and discount codes with the extension API hooks. If you read more data from the React hooks (useXXX()
), the number of log outputs get increased, which means the extension loading code (the registered function = Extension()) gets called multiple times based on how many times data reference APIs are called.
Checkout_attr_discount_2.mp4
Demo 2: How the number of loading executions gets increased or decreased
If you want to avoid unnecessary or unexpected code executions, useEffect hook is one of approach that executes the code only when the values are changed like this code.
- Make your extension code idempotent like handling payment transactions
- Empty data handling for lazy loaded (delayed) data like metafields from React hooks
- Rely on how many times your code is called or expect it runs in singleton
- Expect all data is given in the initial loading
The demo above uses useDiscountCodes and useApplyDiscountCodeChange both in the initial loading which causes infinite loop inside because once the referred items changed, its reference API gets called as reloading regardless of the value changed actually or not. To prevent this, the demo code uses the browser local storage API to check if the value is actually changed or not, storing the results of extension APIs.
*The best practice of preventing this infinite loop is NOT using reading and writing APIs for the same items altogether (e.g. splitting them to other events like button clicks, etc.), but if you have to integrate it for your (merchant's) needs, the storage cache approach above could be an option.
sequenceDiagram
actor Buyer
participant Extension
participant Shopify
Buyer-)Shopify: Enter the checkout flow with some values or input in checkout UI
loop infinite
Shopify-->>Extension: Load the extension code (Extension(), etc)
Extension->>Shopify: Call reference APIs to get the current value (useDiscountCodes, etc.)
Shopify-->>Extension: Return the current value
Extension->>Shopify: Call update APIs to set the given value (useApplyDiscountCodeChange, etc.)
Shopify-->>Extension: Return the API result
end
Diagram 1: How the infinite loop happens
sequenceDiagram
actor Buyer
participant Extension
participant Shopify
Buyer-)Shopify: Enter the checkout flow with some values or input in checkout UI
loop While the value is changed
critical Load the extension code only when the value gets changed
Shopify-->>Extension: Load the extension code (Extension(), etc)
Extension->>Shopify: Call reference APIs to get the current value (useDiscountCodes, etc.)
Shopify-->>Extension: Return the current value
Extension->>Shopify: Check the cash value with Storage API (useStorage.read)
option if the cash value is different from the current
Extension->>Shopify: Call update APIs to set the given value (useApplyDiscountCodeChange, etc.)
Shopify-->>Extension: Return the API result
Extension->>Shopify: Update the cash value with Storage API (useStorage.write)
option if the cash value is the same as the current
Extension->>Extension: Do nothing
end
end
Diagram 2: How to prevent the infinite loop
Most of the extension APIs are supposed to be called asynchronous (async
) especially writing APIs like useApplyDiscountCodeChange, and the extension code to be registered (function Extension()
in the demo code) is NOT async
(if you change the code to async function Extension()
, the extension produces the error in checkout), so the loading code can NOT use await
for those async APIs.
If you want to use await
, you have to wrap the code with your async
functions as follows, but note that the wrapped function call itself is async
in the loading.
export default reactExtension("purchase.checkout.block.render", () => (
<Extension />
));
function Extension() { // This line cannot be `async function Extension()`.
let res = {};
let res2 = {};
const my_func = async () => {
res = await useApplyDiscountCodeChange(); // You can add `await` inside the `async` function only.
};
my_func(); // You cannot add `await` here, so this func is called `async`.
// res = await useApplyDiscountCodeChange(); // This code produces errors.
const my_func2 = async () => {
res2 = await useApplyAttributeChange();
};
my_func2(); // my_func2() doesn't await my_func()
console.log(`${res} ${res2}`); // In this timing, `res` and `res2` are possible to be empty.
...
}
In this case, my_func()
and my_func2()
get executed almost in parallel (async
), so useApplyAttributeChange()
doesn't await useApplyDiscountCodeChange()
(sequential
). if you want to make both sequential
, call them in a single async
function with await
or use the first API's callback.
function Extension() {
let res = {};
let res2 = {};
const my_func = async () => {
res = await useApplyDiscountCodeChange();
res2 = await useApplyAttributeChange();
console.log(`${res} ${res2}`);
};
my_func();
// Or
useApplyDiscountCodeChange().then((r1) => {
res = r1;
useApplyAttributeChange().then((r2) => {
res2 = r2;
console.log(`${res} ${res2}`);
});
});
....
}
When you call those async
APIs multiple times at once (e.g. you call applyCartLinesChange with useApplyAttributeChange for an array of products in a loop, etc.), sometimes this produces an error because those data writing APIs are costly.
const applyCartLinesChange = useApplyCartLinesChange();
const applyAttributeChange = useApplyAttributeChange();
const product_ids = [1001,2002,3003,4004,....]; // Product IDs to add to the current line items and set to the cart attributes
product_ids.map((id) => {
applyCartLinesChange({ // This happens async and you can NOT add `await` because each block of `map` and other loops are NOT async.
type: "addCartLine",
merchandiseId: id,
quantity: 1,
}).then((r1) => {
applyAttributeChange({ // This happens async.
type: 'updateAttribute',
key: 'last_added_product_id',
value: `${id}`
}).then((r2) => {
console.log(`r2: ${JSON.stringify(r2)}`);
});
});
}); // Within this loop, sometimes an error happens due to the overload of heavy API async calls
In this case, you can use recursive function approach which stack each call sequentially as addProducts
function in this code does.
const applyCartLinesChange = useApplyCartLinesChange();
const applyAttributeChange = useApplyAttributeChange();
const product_ids = [1001,2002,3003,4004,....]; // Product IDs to add to the current line items and set to the cart attributes
const addProducts = (i) => { // Define a recursive function to accept the index of the array above.
applyCartLinesChange({ // This happens async.
type: "addCartLine",
merchandiseId: product_ids[i], // Get the id with the given index from the array.
quantity: 1,
}).then((r1) => {
applyAttributeChange({ // This happens async.
type: 'updateAttribute',
key: 'last_added_product_id',
value: `${product_ids[i]}`
}).then((r2) => {
console.log(`r2: ${JSON.stringify(r2)}`);
// If the index is in the middle of array, call the next index `recursively`.
if (i + 1 < product_ids.length) return addProducts(i + 1);
});
});
};
addProducts(0); // Starts with the first id (index 0) of the array.
The following diagram shows the relationship among extensions, source codes and extension targets. In an extension, a target can have a single source code (to be more accurate, single function
to register) only and the source codes (functions) are assembled to a single Web Worker for the extension.
---
config:
layout: elk
elk:
mergeEdges: false
nodePlacementStrategy: LINEAR_SEGMENTS
look: default
theme: default
---
flowchart LR
subgraph EX1["Extension 2"]
srcD1(src/Fizz.jsx<br/>Func-A)
srcS1(src/Buzz.jsx<br/>Func-B<br/>Func-C)
end
subgraph EX2["Extension 1"]
srcD2(src/Hello.jsx<br/>Func-A)
srcS2(src/World.jsx<br/>Func-B)
end
subgraph TG1["Checkout Targets"]
tS1(Static: purchase.checkout.actions.render-before)
tD(Dynamic: purchase.checkout.block.render)
tS2(Static: purchase.checkout.contact.render-after)
end
subgraph WW1["Web Worker of Extension 2"]
srcW1(456/yyy.js<br/>Fizz.Func-A<br/>Buzz.Func-B<br/>Buzz.Func-C)
end
subgraph WW2["Web Worker of Extension 1"]
srcW2(123/xxx.js<br/>Hello.Func-A<br/>World.Func-B)
end
srcD1 --> tD
srcS1 --> tS1
srcS1 --> tS2
srcD1 -.->|❌| tS1
srcD2 --> tD
srcS2 --> tS1
srcS2 -.->|❌| tD
tD --> srcW1
tS1 --> srcW1
tS2 --> srcW1
tD --> srcW2
tS1 --> srcW2
EX1 ==> WW1
EX2 ==> WW2
classDef extension stroke-width:4px
classDef source fill:#FFFFFF
classDef worker fill:#E5FFBF
classDef static fill:#FFC0CB
classDef dynamic fill:#BFBFFF
class EX1 extension
class EX2 extension
class WW1 extension
class WW2 extension
class srcD1 source
class srcS1 source
class srcD2 source
class srcS2 source
class srcW1 worker
class srcW2 worker
class tS1 static
class tS2 static
class tD dynamic
- You can implement separated functions in separated source files (
Func-A
inHello.jsx
+Func-B
inWorld.jsx
case above) like these extension codes with this configuration file. - You can implement separated functions in a single file (
Func-B
+Func-C
inBuzz.jsx
case above) like this extension code with this configuration file.
Note that if you set duplicated source codes (functions) to a single target in a configuration file, shopify app deploy
command produces an error with a message like Duplicate targets found: purchase.checkout.block.render Extension point targets must be unique
.
If you want to create a common (shared) code among the extension functions, the following approaches work.
- Among multiple source files (
Hello.jsx
+World.jsx
case above), create a separated file to be imported by each source file to be called by each registered function likecommonFuncExternal
in the demo code. - In a single source file (
Buzz.jsx
case above), simply create a function in it to be called by each registered function likecommonFuncInFile
in the demo code.
The number of Web Workers in browsers differs on how many extension blocks are added in checkout editors. For example, if you have an extension for dynamic targets, you can add multiple blocks to some areas in checkout. In this case, the number of generated Web Worker instances are the same as the added blocks (i.e. if you add the two same blocks in the editor, two Web Workers run made of the same code).

Extension blocks and Web Worker instances
This means, if you add some blocks from the same extension, each block runs independently not affecting each other and the number of loadings is multiplied by the same number of blocks (i.e. if you add the two same blocks in the editor, the extension code is called double in the browser).


Number of extension blocks multiplies the number of loadings
The storage API used by the demo above wraps LocalStorage APIs and the actual keys of the storage are added some unique keys by Shopify which prevents key conflicts with other extension blocks.

How the extension storage API stores the data in browsers
*Note that the multiple blocks from the same extension run independently, but the used key of the local storage by the same extension is the same (shared), so the local caching process affects each other (or you can say that you get the benefit of sharing data among Web Workers from the same extension).
Shopify.dev supports React and Vanilla JS both for writing the UI extensions, and this sample app has two source codes of Checkout.jsx (React) and Checkout.js (Vanilla JS) which you can switch in the configuration file to check how the source codes and behaviors differ.
The following shows the major differences between React and Vanilla JS implementation.
React | Vanila JS | |
---|---|---|
How to access Shopify provided data | You can choose React hooks (useXXX()) or the same way as Vanilla JS👉 | Subscribe functions (api.XXX.subscribe((entry) => {})) (some data like settings can be accessed through api.XXX.current directly) |
Multiple loading of the extension code | Yes (when the data access hooks are used, especially reading metafields triggers many loadings) | No (the only subscribe function's callback = (entry) => {} get called multiple times) |
Reactivate UI automatically | Yes (React UI components render the data automatically when the referred variables get changed) | No (UI component rendering needs to be called again when the referred variables get changed) |
UI implementation | Simple (DOM syntax with JSX makes extension code simpler) |
Complex (all DOM generation need to be root.createComponent() stacks without status handling APIs) |
-
Can I receive events when buyers change fields in checkout UI? (e.g. when the buyer changes their shipping address)
Yes: As described in the infinite loop section, if the field referred by the code changes, Shopify calls the extension code (function) or API subscribe again, so just use
React hooks = useXXX() or api.XXX.subscribe((entry) => {})
can detect the data change. Note that events NOT involving data change likePay now
button click, or unsupported fields by React hooks or API subscribe can NOT be received (in this case, Web Pixels events might work but those events can be received only, can NOT be used for impacting checkout like blocking or changing data, etc., just for sending data to other places only).
This document is NOT an official tutorial or specification from Shopify, just use for helping you to implement Shopify Checkout UI Extension effectively and fit more use cases. The information described here is possible to be changed by Shopify future updates.