Using Polaris in Shopify Admin | Shopify App Development
Getting started with Shopify's Polaris Components can be daunting. In this guide I'll show you how to create a quick Shopify App project where you can utilize a few popular Polaris components to make your interface look cohesive with the Shopify Admin.
Code: https://github.com/ndrishinski/bulk-update-tags/blob/main/app/routes/app.addTags.jsx
Prefer video?
Enjoy, otherwise read on!
Scaffolding an App
The first thing we need to do is scaffold a Remix app using the Shopify CLI. To get started run:
`shopify app init` and give your project a name, select 'Build a Remix app' and 'Javascript' as the language.
After this is completed, in the terminal we can 'cd' into our new project and run shopify app dev to start our local development server.
This will ensure you are logged into a Shopify partner account, where you can select organization and store you'd like to preview this on.
Create our New File and Route
Now that our app is running, we can navigate in our text editor the the 'routes' directory within 'app'. Now the Remix convention is to add a file with app.<name of file> for a new route. We are going to create a file called `app.addTags.jsx`. In this new file we can add the following code:
// app/routes/app.addTags.jsx
import React, { useState, useCallback, useEffect } from 'react';
import {
Card,
ResourceList,
Avatar,
ResourceItem,
Text,
InlineGrid,
TextField,
} from '@shopify/polaris';
import {DeleteIcon} from '@shopify/polaris-icons';
import { authenticate } from '../shopify.server'
import {
useLoaderData,
useActionData,
useSubmit,
Form,
} from "@remix-run/react";
import { json } from '@remix-run/node';
export const loader = async ({ request }) => {
const { admin } = await authenticate.admin(request)
try {
const response = await admin.graphql(
`{
products(first: 10) {
edges {
node {
id
title
featuredMedia {
id
preview {
image {
id
url
}
}
}
tags
}
}
}
}`
)
const parsed = await response.json()
return json({ data: parsed})
} catch (error) {
console.error('Error fetching products:', error)
return json({ data: [] })
}
};
export const action = async ({ request }) => {
// Authenticate admin
const { admin } = await authenticate.admin(request);
// Parse the form data
const formData = await request.formData();
const productIds = JSON.parse(formData.get('productIds')) || [] // Array of product IDs
console.log('testing server prod Ids: ', productIds)
const tag = formData.get('tag'); // Tag string
const isRemoval = formData.get('isRemoval')
// Validate input
if (!productIds || productIds.length === 0 || !tag) {
console.error('Invalid input: Product IDs or tag missing.');
return json({ success: false, message: 'Product IDs or tag missing.' });
}
const mutation = `
mutation ${isRemoval === 'true' ? 'removeTags' : 'addTags'}($id: ID!, $tags: [String!]!) {
${isRemoval === 'true' ? 'tagsRemove' : 'tagsAdd'}(id: $id, tags: $tags) {
node {
id
}
userErrors {
message
}
}
}
`;
// Iterate over productIds and make GraphQL calls
const results = await Promise.all(productIds.map(async (prodId) => {
try {
// Prepare variables for the mutation
const variables = {
id: prodId,
tags: [tag]
};
// Execute the GraphQL mutation
const response = await admin.graphql(mutation, { variables });
const responseData = await response.json();
const data = responseData.data
const { userErrors } = data;
if (userErrors && userErrors.length > 0) {
console.error(`User errors for product ${prodId}:`, userErrors);
return { productId: prodId, success: false, userErrors };
} else {
console.log(`Successfully updated product ${prodId} with tag "${tag}".`);
return { productId: prodId, success: true };
}
} catch (error) {
console.error(`Failed to update product ${prodId}:`, error);
return { productId: prodId, success: false, error: error.message };
}
}));
// Return the results
return json({ success: true, results });
};
const AppAddTags = () => {
const [selectedItems, setSelectedItems] = useState([]);
const allProducts = useLoaderData();
const data = useActionData()
const submit = useSubmit()
const [value, setValue] = useState('');
const [allProds, setAllProds] = useState([]);
const [successful, setSuccessful] = useState(false);
const [isRemoval, setIsRemoval] = useState(false);
const handleChange = useCallback(
(newValue) => setValue(newValue),
[],
);
useEffect(() => {
if (data?.success) {
setSuccessful(true);
setTimeout(() => setSuccessful(false), 2000);
}
}, [data]);
useEffect(() => {
const tempProducts = allProducts.data.data.products.edges;
const updatedAllProds = tempProducts.map(i => {
return {id: i.node.id, title: i.node.title, featuredMedia: i.node.featuredMedia?.preview?.image?.url || '', tags: i.node.tags || []}
});
setAllProds([...updatedAllProds]);
}, [allProducts]);
const handleTagUpate = (event) => {
const formData = new FormData();
formData.append('productIds', JSON.stringify(selectedItems));
formData.append('tag', value);
formData.append('isRemoval', isRemoval)
submit(formData, { method: 'post', encType: 'multipart/form-data' });
setSelectedItems([])
};
const resourceName = {
singular: 'Product',
plural: 'Products'
}
const promotedBulkActions = [
{
content: 'Add Tagz',
onAction: (e) => {
setIsRemoval(false)
handleTagUpate(e)
},
},
{
content: 'Remove Tag',
onAction: (e) => {
setIsRemoval(true)
handleTagUpate(e)
},
},
];
const bulkActions = [
{
content: 'Add tags',
onAction: () => console.log('Todo: implement bulk add tags'),
},
{
content: 'Remove tags',
onAction: () => console.log('Todo: implement bulk remove tags'),
},
{
icon: DeleteIcon,
destructive: true,
content: 'Delete customers',
onAction: () => console.log('Todo: implement bulk delete'),
},
];
return (
<Card>
{successful && (
<Text variant="headingXl" as="h4">Successfully Updated</Text>
)}
<Form method="POST">
<TextField
label="Tag Add / Removal"
value={value}
onChange={handleChange}
autoComplete="off"
/>
<ResourceList
resourceName={resourceName}
items={allProds}
renderItem={renderItem}
selectedItems={selectedItems}
onSelectionChange={setSelectedItems}
promotedBulkActions={promotedBulkActions}
bulkActions={bulkActions}
/>
</Form>
</Card>
);
};
export default AppAddTags;
function renderItem({id, title, featuredMedia, tags}) {
const media = <Avatar customer={false} size="md" name={title} source={featuredMedia} />;
return (
<ResourceItem
id={id}
url={'#'}
media={media}
accessibilityLabel={`View details for ${title}`}
>
<InlineGrid columns={3}>
<Text variant="bodyMd" fontWeight="bold" as="h3">
{title}
</Text>
<Text variant="bodyMd">
{tags.map((tag) => (
<span key={tag} style={{ marginLeft: '8px' }}>
{tag}
</span>
))}
</Text>
<div>ID: {id}</div>
</InlineGrid>
</ResourceItem>
);
}
Now that is a lot of code. The main things I will point out are the loader function which Remix uses to run server side queries for us when the component is intialized. We are using it to query the first 10 products of our store which is then returned to our component to show in our list.
Similarly we have the action function which is used to actually modify the products with the tags we want to add or remove. It is also run server side and we use the Form element to run that code.
Lastly, I would recommend reading the documentation here. Within those docs you will find all the components that we are using from Polaris along with the different props and styling options that are available to us.
Update our Navigation
Now that our code is in place, we can simply update our navigation to show us this new route we created. In the 'app.jsx' file under 'routes' directory, we can update the Link to our new route like so:
<AppProvider isEmbeddedApp apiKey={apiKey}>
<NavMenu>
<Link to="/app" rel="home">
Home
</Link>
<Link to="/app/addTags">Add / Remove Tags</Link>
</NavMenu>
<Outlet />
</AppProvider>
Now when we click the Add / Remove Tags link in the admin, we should see our newly created file.

Conclusion
I hope this guide has been helpful in learning how to use Polaris to create a Shopify admin interface. Make sure to subscribe to my youtube channel for more tutorials like this.