How do i create a metafield on form submit

Topic summary

Issue: A developer new to React and Shopify is attempting to save a form-submitted key value to a metafield definition, but encounters an error where the admin object is undefined when the action is triggered, preventing the GraphQL mutation from executing.

Code Problem:

  • The admin object exists in the loader but returns undefined in the action function
  • GraphQL mutation never triggers due to missing admin context
  • Code includes reversed/garbled text sections, suggesting formatting issues

Resolution Provided:

  • Another user (zano) successfully solved the same problem by referencing three key documentation sources:
    • Remix use-submit hooks documentation
    • Polaris Form custom onSubmit examples
    • Shopify QR code example app

Solution Pattern:

  • Authenticate admin in both loader and action functions
  • Use useSubmit hook to handle form submission with {method: "POST", replace: true}
  • Implement handleFormSubmit callback that calls submit with form data
  • Read current metafield values in loader, update/create in action

Follow-up Question:
Original poster asks for clarification on how the GraphQL query was executed in the action, since admin.graphql method should exist but wasn’t working in their implementation.

Summarized with AI on November 13. AI used: claude-sonnet-4-5-20250929.

Here is my app._index.jsx file. I am new to the react and the Shopify model. I am trying to get the key from the formsubmit and save that value to metafield definition so each store/shop can have their own key saved. I want to use this key in my app at later stage. Think of this as a GA config key etc. The path to shopifyserver is actual path because ../shopify.server.js path was not being found.

I can see that the admin exists on the load but not when the action is triggered and i get undefined for the admin and the graphql is never triggered . Hope you can help.

import { useEffect } from "react";
import { json } from "@remix-run/node";
import { authenticate } from "/Users/ja/Desktop/ShopifyApp/new-data-publisher/app/shopify.server.js";
import { useLoaderData,useActionData, useNavigation, useSubmit } from "@remix-run/react";
import {
  Page,
  Layout,
  Text,
  Card,
  Button,
  BlockStack,
  Box,
  List,
  Link,
  InlineStack,
  Form
} from "@shopify/polaris";

//console.log( authenticate);
export const loader = async ({ request }) => {
  const { admin } = await authenticate.admin(request);
  //return { admin }
  return null;

};

export async function action({ request, newappkeyValue }) {
  try {
    const { admin } = await authenticate?.admin(request);

    // Ensure admin object is available before proceeding
    if (!admin) {
      console.error('Error: Admin object not available.');
      return json({ error: 'Admin object not available' }, { status: 500 });
    }

    const newAppKey = newappkeyValue;
    const response = await admin.graphql(`
      mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) {
        metafieldDefinitionCreate(definition: $definition) {
          createdDefinition {
            id
            name
            namespace
            key
            description
          }
          userErrors {
            field
            message
            code
          }
        }
      }
    `, {
      variables: {
        definition: {
          name: "newAppkey",
          namespace: "$app:new-appkey",
          key: `${newAppKey}`, 
          description: "stores customers new app key.",
          type: "list.single_line_text_field"
        }
      },
    });

    const responseJson = await response.json();
    return json({
      createdDefinition: responseJson.data.metafieldDefinitionCreate.createdDefinition,
      userErrors: responseJson.data.metafieldDefinitionCreate.userErrors,
    });
  } catch (error) {
    console.error('Error in action:', error.message);
    return json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

export default function Index() {
  const nav = useNavigation();
  const actionData = useActionData();
  const submit = useSubmit();
  const isLoading =
    ["loading", "submitting"].includes(nav.state) && nav.formMethod === "POST";
  //const newAppKey = actionData?.product?.id.replace(
   // "gid://shopify/Product/",
   // ""
  //);

  //useEffect(() => {
   // if (newAppKey) {
     // shopify.toast.show("newAppKey Added");
    //}
  //}, [newAppKey]);
  //const generateProduct = () => submit({}, { replace: true, method: "POST" });

  return (
    
  );
}

Hi, i have the same problem.
Have you already solved it?

Hi, I finally did it.
I found these 3 documents:

I did something like this:

export const loader = async ({ request }) => {
  const { admin, session } = await authenticate.admin(request);

  const responseJson = await FuntionToReadMetafields();

  return json({
    data: responseJson.data
  });
};

export async function action({ request, params }) {
  // Use the admin client to create or update metafields
  const { admin, session } = await authenticate.admin(request);
  const { shop } = session;

  // read data from MyForm
  const formData = {
    ...Object.fromEntries(await request.formData()),
    shop,
  };

  // read single value
  const { v1 } = formData;

  FuntionToCreateOrUpdateMetafieldsMetafields();
}

const MyForm = ({ onSubmit }) => {
  const formData = useLoaderData(); // read the current metafields value
  
  const [v1, setV1Value] = useState(myFunction(formData));
  const handleV1Change = useCallback(
    (value) => setV1Value(value),
    [],
  );

  // callback onSubmit with form data
  const buttonClickAction = (event) => {
    event.preventDefault();
    onSubmit({ v1 });
  };

  return (
    
  );
};

export default function Index() {
  const nav = useNavigation();
  const submit = useSubmit();
  const actionData = useActionData();

  // call submit with form data to save
  const handleFormSubmit = async (formData) => {
    submit(formData, { replace: true, method: "POST" } );
  };

  return (
    
  );
};

How did it work, On form submit to save a metafield value you would need to run a graphql query and will need admin.graphql method but this does not exist on action. Can you please share how did you update the metafields?

1 Like