Reading app owned metafields from checkout UI extension not working

Using the reserved $app namespace when reading metafields doesn’t seem to work. Setting metafields via applyMetafieldChange on the app owned namespace works just fine, but reading them with useAppMetafields always produces an empty array. I’ve made sure my api_version is 2025-07 and i followed the guide below exactly. As soon as i use a non app owned namespace, everything works fine. Any ideas?

TOML

[[extensions.metafields]]
namespace = "$app"
key = "my-key"

React extension code (this is always undefined, even though I confirmed the metafield exists)

  const [discount] = useAppMetafields({
    type: "cart",
    namespace: "$app",
    key: "my-key",
  });

@aaron_barbieri have you tried setting a namespace like namespace ="$app:some-namespace" in the TOML rather than namespace = "$app"?

@James_Vidler yup that doesn’t seem to work either.

@James_Vidler any updates on this? Seems like a bug to me

Does this help?

i don’t think so. What im saying is that if you follow this shopify dev guide, it doesn’t work for cart metafields.

Literally copy the example code, set a cart metafield with useApplyMetafieldsChange and type: "updateCartMetafield", and try to read it back with useAppMetafields just like the documentation does. It won’t work!

Encountered the same issue today and not working with either app owned namespace or not, it must be a bug.

Hi folks @aaron_barbieri @Andrew_Jaanuu, are you running into this issue using an extension preview? If so, could you try deploying the extension and see if you see a similar issue with the deployed extension? I’m trying to narrow down where the problem is. Thanks in advance

@Andrew_Jaanuu I see it working just fine if you use a regular (non app owned) namespace. The issue is that when you use namespace = "$app" useAppMetafields is always undefined.

@avocadomayo hey Wendy, appreciate you hopping on this. I’m honestly not sure if it works when the extension is deployed, but I can’t easily test it since it would require me to update the app source code (in addition to the extension code, which isn’t a problem) and would impact real life customers.

My bigger concern here is that the official Shopify documentation has a section on this, yet it doesn’t seem to work. Are you able to provide an estimate of when this might be resolved? Inability to use app owned namespaces means that i’m currently using unprotected namespaces in production, which seems like a bit of a security concern.

@avocadomayo It makes no difference for me whether I use namespace = “$app” or not. I created “ownerType”: “CUSTOMER” type of metafield definition under the $app namespace and it useAppMetafields shows empty when checkout was loading for the first time, It actually takes a couple of rerender of the extension react component for it to finally show the metafield I had.

The test store I used was a shopify plus dev store under the next-gen dev platform. It is the same whether it is preview or deployed extension.

Update: I deleted the metafield definition and just created the unstructured “$app” metafield and it still works the same.

Hi @aaron_barbieri, I understand the concern around testing on a live app. Would it be possible for you to see if the deployed version works in a test extension or a test app, on a dev shop?

Based on our testing, the issue appears to be isolated to extension preview and we would like to confirm with you that this is exactly what you are running into, or if there is something else at play

Hi @Andrew_Jaanuu, do you mean useAppMetafields is empty on Checkout’s initial render, but populates in subsequent renders? That sounds like expected behaviour - your UI extension should update automatically with the values as they populate.

Can you help me understand the issue better? Video or code helpful.

Yes, it is empty on Checkout’s initial render, but populates in subsequent renders when I was not refreshing the page, which looks like a bug because others like useCustomer renders right away and the app metafield should have been available at the same time.

Example code for this:

import {
  reactExtension,
  Banner,
  BlockStack,
  Text,
  Button,
  useApi,
  useTranslate,
  useApplyGiftCardChange,
  useAppliedGiftCards,
  useAppMetafields,
  useAttributes,
  useApplyAttributeChange,
  useCustomer
} from "@shopify/ui-extensions-react/checkout";
import {useEffect, useRef, useState} from 'react';

// 1. Choose an extension target
export default reactExtension("purchase.checkout.block.render", () => (
  <Extension />
));

function Extension() {
  const translate = useTranslate();
  const { extension } = useApi();

  const applyGiftCardChange = useApplyGiftCardChange();
  const appliedGiftCards = useAppliedGiftCards();
  const appMetafields = useAppMetafields(); // Fetch customer metafields
  const customer = useCustomer(); // Fetch customer data if needed

  const appliedRef = useRef(false); // only true after a successful apply
  const processingRef = useRef(false);
  
  const attributes = useAttributes();
  const applyAttributeChange = useApplyAttributeChange();
  const customerIdGiftCardApplication = attributes?.find(a => a.key === 'customerIdGiftCardApplication');

  const [status, setStatus] = useState<'idle' | 'removing' | 'applying' | 'success' | 'error'>('idle');
  const [message, setMessage] = useState<string | undefined>();

  console.log("app metafields", appMetafields);

  console.log("customer", customer);

  return null;
}

it prints app metafields [], while customer[{id:...}] for a few time and then app metafields [{something}], and customer[{id:...}] later.

Hi @Andrew_Jaanuu,

In checkout (for performance reasons), app metafields may be provided after the initial render of your UI extension in some cases. As long you subscribe to the change (like you are doing using useAppMetafields()), this impact is usually negligible. We are aware of this quirk though and understand it can be difficult to act on the state of checkout when you aren’t sure if you have the resolved app metafields or not. We do have plans to address this, but we don’t have any further details to share at this time.

@James_Vidler, @avocadomayo,

What if I’m using the non-React version, like in this example, how can I subscribe to access the Metafield data?

The documentation of the non-React version only shows direct access like api.appMetafields.current, not via function, but via a property.

Hi @flavio-b,

Using the JS version, you can subscribe to metafield updates and re-render your extension accordingly.

api.appMetafields.subscribe((entries) => {
  // re-render your extension with updated metafields
});

Please see a full example in the JavaScript code example tab on this page.

@avocadomayo, thank you! That clarifies it.

So, pretty much most of the properties we’ve been accessing with api.something.current need to actually use api.something.subscribe()?

I thought that was more if we wanted to catch live updates to those properties for some reason, and current appeared to work fine for everything, at least in production.

Even if the initial value is populated, it is never guaranteed to be static. For best practices, we recommend (1) reading the current value for the initial render, and (2) subscribing to updates to re-render your extension as needed.

1 Like

Got it, thank you!

For the few properties we access, technically we don’t need to catch live updates, so it seems like using current would be enough.

It is only api.appMetafields.current that takes a moment to load when running in the Editor, so we’ll use subscribe for that, but my understanding was that all current fields should be available right from the start. I’m unsure if this delay is more of a “quirk”, or the normal expected behaviour.

@avocadomayo I am seeing a similar problem here, where useAppMetafields() returns blank when I am running shopify app dev, but if I deploy, I see the metafields being returned. I’ve only tried with product metafields (so not with metafields owned by the app).

In the preview version, I am seeing a 401 in the console for a request to /private_access_tokens (in case that is related)