---
title: Build a Shopify app using React Router
description: Learn how to build a Shopify app using React Router, Polaris web components, App Bridge and Prisma.
source_url:
html: https://shopify.dev/docs/apps/build/build?framework=reactRouter
md: https://shopify.dev/docs/apps/build/build.md?framework=reactRouter
---
# Build a Shopify app using React Router
After you scaffold an app, you can add your own functionality to pages inside and outside of the Shopify admin.
In this tutorial, you'll scaffold an app that makes QR codes for products. When the QR code is scanned, it takes the user to a checkout that's populated with the product, or to the product page. The app logs every time the QR code is scanned, and exposes scan metrics to the app user.
Follow along with this tutorial to build a sample app, or clone the completed sample app.
## What you'll learn
In this tutorial, you'll learn how to do the following tasks:
* Update the [Prisma](https://www.prisma.io/) database included in the app template.
* Use the [@shopify/shopify-app-react-router](https://www.npmjs.com/package/@shopify/shopify-app-react-router) package to authenticate users and query data.
* Use [Polaris web components](https://shopify.dev/docs/api/app-home/polaris-web-components) to create a UI that adheres to Shopify's [App Design Guidelines](https://shopify.dev/docs/apps/design-guidelines).
* Use [Shopify App Bridge](https://shopify.dev/docs/api/app-bridge) to add interactivity to your app.
## Requirements
[Scaffold an app](https://shopify.dev/docs/apps/build/scaffold-app)
Scaffold an app that uses the [React Router template](https://github.com/Shopify/shopify-app-template-react-router).
[Install `qrcode`](https://www.npmjs.com/package/qrcode)
Enables creation of QR codes.
[Install `@shopify/polaris-icons`](https://www.npmjs.com/package/@shopify/polaris-icons)
Provides placeholder images for the UI.
[Install `tiny-invariant`](https://www.npmjs.com/package/tiny-invariant)
Allows loaders to easily throw errors.
## Project

React Router
[View on GitHub](https://github.com/Shopify/example-app--qr-code--remix/blob/upgrade-to-react-router/)
## Add the QR code data model to your database
To store your QR codes, you need to add a table to the database included in your template.
Info
The single table in the template's Prisma schema is the `Session` table. It stores the tokens for each store that installs your app, and is used by the [@shopify/shopify-app-session-storage-prisma](https://www.npmjs.com/package/@shopify/shopify-app-session-storage-prisma) package to manage sessions.
### Create the table
Add a `QRCode` model to your Prisma schema. The model should contain the following fields:
* `id`: The primary key for the table.
* `title`: The app user-specified name for the QR code.
* `shop`: The store that owns the QR code.
* `productId`: The product that this QR code is for.
* `productHandle`: Used to create the destination URL for the QR code.
* `productVariantId`: Used to create the destination URL for the QR code.
* `destination`: The destination for the QR code.
* `scans`: The number times that the QR code been scanned.
* `createdAt`: The date and time when the QR code was created.
***
The `QRCode` model contains the key identifiers that the app uses to retrieve Shopify product and variant data. At runtime, additional product and variant properties are retrieved and used to populate the UI.
## /prisma/schema.prisma
```prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:dev.sqlite"
}
model Session {
id String @id
shop String
state String
isOnline Boolean @default(false)
scope String?
expires DateTime?
accessToken String
userId BigInt?
}
model QRCode {
id Int @id @default(autoincrement())
title String
shop String
productId String
productHandle String
productVariantId String
destination String
scans Int @default(0)
createdAt DateTime @default(now())
}
```
### Migrate the database
After you add your schema, you need to migrate the database to create the table.
1. Run the following command to create the table in Prisma:
## Terminal
```bash
npm run prisma migrate dev -- --name add-qrcode-table
```
```bash
yarn prisma migrate dev --name add-qrcode-table
```
```bash
pnpm run prisma migrate dev --name add-qrcode-table
```
2. To confirm that your migration worked, open [Prisma Studio](https://www.prisma.io/docs/concepts/components/prisma-studio):
## Terminal
```bash
npm run prisma studio
```
```bash
yarn prisma studio
```
```bash
pnpm run prisma studio
```
Prisma Studio opens in your browser.
3. In Prisma Studio, click the **QRCode** tab to view the table.
You should see a table with the columns that you created, and no data.
## Get QR code and product data
After you create your database, add code to retrieve data from the table.
Supplement the QR code data in the database with product information.
### Create the model
Create a model to get and validate QR codes.
Create an `/app/models` folder. In that folder, create a new file called `QRCode.server.js`.
### Get QR codes
Create a function to get a single QR code for your QR code form, and a second function to get multiple QR codes for your app's index page. You'll [create a QR code form](#create-a-qr-code-form) later in this tutorial.
QR codes stored in the database can be retrieved using the Prisma `FindFirst` and `FindMany` queries.
## /app/models/QRCode.server.js
```javascript
import qrcode from "qrcode";
import invariant from "tiny-invariant";
import db from "../db.server";
export async function getQRCode(id, graphql) {
const qrCode = await db.qRCode.findFirst({ where: { id } });
if (!qrCode) {
return null;
}
return supplementQRCode(qrCode, graphql);
}
export async function getQRCodes(shop, graphql) {
const qrCodes = await db.qRCode.findMany({
where: { shop },
orderBy: { id: "desc" },
});
if (qrCodes.length === 0) return [];
return Promise.all(
qrCodes.map((qrCode) => supplementQRCode(qrCode, graphql))
);
}
export function getQRCodeImage(id) {
const url = new URL(`/qrcodes/${id}/scan`, process.env.SHOPIFY_APP_URL);
return qrcode.toDataURL(url.href);
}
export function getDestinationUrl(qrCode) {
if (qrCode.destination === "product") {
return `https://${qrCode.shop}/products/${qrCode.productHandle}`;
}
const match = /gid:\/\/shopify\/ProductVariant\/([0-9]+)/.exec(qrCode.productVariantId);
invariant(match, "Unrecognized product variant ID");
return `https://${qrCode.shop}/cart/${match[1]}:1`;
}
async function supplementQRCode(qrCode, graphql) {
const qrCodeImagePromise = getQRCodeImage(qrCode.id);
const response = await graphql(
`
query supplementQRCode($id: ID!) {
product(id: $id) {
title
media(first: 1) {
nodes {
preview {
image {
altText
url
}
}
}
}
}
}
`,
{
variables: {
id: qrCode.productId,
},
}
);
const {
data: { product },
} = await response.json();
return {
...qrCode,
productDeleted: !product?.title,
productTitle: product?.title,
productImage: product?.media?.nodes[0]?.preview?.image?.url,
productAlt: product?.media?.nodes[0]?.preview?.image?.altText,
destinationUrl: getDestinationUrl(qrCode),
image: await qrCodeImagePromise,
};
}
export function validateQRCode(data) {
const errors = {};
if (!data.title) {
errors.title = "Title is required";
}
if (!data.productId) {
errors.productId = "Product is required";
}
if (!data.destination) {
errors.destination = "Destination is required";
}
if (Object.keys(errors).length) {
return errors;
}
}
```
### Get the QR code image
A QR code takes the user to `/qrcodes/$id/scan`, where `$id` is the ID of the QR code. Create a function to construct this URL, and then use the `qrcode` package to return a base 64-encoded QR code image `src`.
***
## /app/models/QRCode.server.js
```javascript
import qrcode from "qrcode";
import invariant from "tiny-invariant";
import db from "../db.server";
export async function getQRCode(id, graphql) {
const qrCode = await db.qRCode.findFirst({ where: { id } });
if (!qrCode) {
return null;
}
return supplementQRCode(qrCode, graphql);
}
export async function getQRCodes(shop, graphql) {
const qrCodes = await db.qRCode.findMany({
where: { shop },
orderBy: { id: "desc" },
});
if (qrCodes.length === 0) return [];
return Promise.all(
qrCodes.map((qrCode) => supplementQRCode(qrCode, graphql))
);
}
export function getQRCodeImage(id) {
const url = new URL(`/qrcodes/${id}/scan`, process.env.SHOPIFY_APP_URL);
return qrcode.toDataURL(url.href);
}
export function getDestinationUrl(qrCode) {
if (qrCode.destination === "product") {
return `https://${qrCode.shop}/products/${qrCode.productHandle}`;
}
const match = /gid:\/\/shopify\/ProductVariant\/([0-9]+)/.exec(qrCode.productVariantId);
invariant(match, "Unrecognized product variant ID");
return `https://${qrCode.shop}/cart/${match[1]}:1`;
}
async function supplementQRCode(qrCode, graphql) {
const qrCodeImagePromise = getQRCodeImage(qrCode.id);
const response = await graphql(
`
query supplementQRCode($id: ID!) {
product(id: $id) {
title
media(first: 1) {
nodes {
preview {
image {
altText
url
}
}
}
}
}
}
`,
{
variables: {
id: qrCode.productId,
},
}
);
const {
data: { product },
} = await response.json();
return {
...qrCode,
productDeleted: !product?.title,
productTitle: product?.title,
productImage: product?.media?.nodes[0]?.preview?.image?.url,
productAlt: product?.media?.nodes[0]?.preview?.image?.altText,
destinationUrl: getDestinationUrl(qrCode),
image: await qrCodeImagePromise,
};
}
export function validateQRCode(data) {
const errors = {};
if (!data.title) {
errors.title = "Title is required";
}
if (!data.productId) {
errors.productId = "Product is required";
}
if (!data.destination) {
errors.destination = "Destination is required";
}
if (Object.keys(errors).length) {
return errors;
}
}
```
### Get the destination URL
Scanning a QR code takes the user to one of two places:
* The product details page
* A checkout with the product in the cart
Create a function to conditionally construct this URL depending on the destination that the merchant selects.
## /app/models/QRCode.server.js
```javascript
import qrcode from "qrcode";
import invariant from "tiny-invariant";
import db from "../db.server";
export async function getQRCode(id, graphql) {
const qrCode = await db.qRCode.findFirst({ where: { id } });
if (!qrCode) {
return null;
}
return supplementQRCode(qrCode, graphql);
}
export async function getQRCodes(shop, graphql) {
const qrCodes = await db.qRCode.findMany({
where: { shop },
orderBy: { id: "desc" },
});
if (qrCodes.length === 0) return [];
return Promise.all(
qrCodes.map((qrCode) => supplementQRCode(qrCode, graphql))
);
}
export function getQRCodeImage(id) {
const url = new URL(`/qrcodes/${id}/scan`, process.env.SHOPIFY_APP_URL);
return qrcode.toDataURL(url.href);
}
export function getDestinationUrl(qrCode) {
if (qrCode.destination === "product") {
return `https://${qrCode.shop}/products/${qrCode.productHandle}`;
}
const match = /gid:\/\/shopify\/ProductVariant\/([0-9]+)/.exec(qrCode.productVariantId);
invariant(match, "Unrecognized product variant ID");
return `https://${qrCode.shop}/cart/${match[1]}:1`;
}
async function supplementQRCode(qrCode, graphql) {
const qrCodeImagePromise = getQRCodeImage(qrCode.id);
const response = await graphql(
`
query supplementQRCode($id: ID!) {
product(id: $id) {
title
media(first: 1) {
nodes {
preview {
image {
altText
url
}
}
}
}
}
}
`,
{
variables: {
id: qrCode.productId,
},
}
);
const {
data: { product },
} = await response.json();
return {
...qrCode,
productDeleted: !product?.title,
productTitle: product?.title,
productImage: product?.media?.nodes[0]?.preview?.image?.url,
productAlt: product?.media?.nodes[0]?.preview?.image?.altText,
destinationUrl: getDestinationUrl(qrCode),
image: await qrCodeImagePromise,
};
}
export function validateQRCode(data) {
const errors = {};
if (!data.title) {
errors.title = "Title is required";
}
if (!data.productId) {
errors.productId = "Product is required";
}
if (!data.destination) {
errors.destination = "Destination is required";
}
if (Object.keys(errors).length) {
return errors;
}
}
```
### Retrieve additional product and variant data
The QR code from Prisma needs to be supplemented with product data. It also needs the QR code image and destination URL.
Create a function that queries the [GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql) for the product title, and the first featured product image's URL and alt text. It should also return an object with the QR code data and product data, and use the `getDestinationUrl` and `getQRCodeImage` functions that you created to get the destination URL's QR code image.
***
[](https://shopify.dev/docs/api/admin-graphql)
[GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql)
## /app/models/QRCode.server.js
```javascript
import qrcode from "qrcode";
import invariant from "tiny-invariant";
import db from "../db.server";
export async function getQRCode(id, graphql) {
const qrCode = await db.qRCode.findFirst({ where: { id } });
if (!qrCode) {
return null;
}
return supplementQRCode(qrCode, graphql);
}
export async function getQRCodes(shop, graphql) {
const qrCodes = await db.qRCode.findMany({
where: { shop },
orderBy: { id: "desc" },
});
if (qrCodes.length === 0) return [];
return Promise.all(
qrCodes.map((qrCode) => supplementQRCode(qrCode, graphql))
);
}
export function getQRCodeImage(id) {
const url = new URL(`/qrcodes/${id}/scan`, process.env.SHOPIFY_APP_URL);
return qrcode.toDataURL(url.href);
}
export function getDestinationUrl(qrCode) {
if (qrCode.destination === "product") {
return `https://${qrCode.shop}/products/${qrCode.productHandle}`;
}
const match = /gid:\/\/shopify\/ProductVariant\/([0-9]+)/.exec(qrCode.productVariantId);
invariant(match, "Unrecognized product variant ID");
return `https://${qrCode.shop}/cart/${match[1]}:1`;
}
async function supplementQRCode(qrCode, graphql) {
const qrCodeImagePromise = getQRCodeImage(qrCode.id);
const response = await graphql(
`
query supplementQRCode($id: ID!) {
product(id: $id) {
title
media(first: 1) {
nodes {
preview {
image {
altText
url
}
}
}
}
}
}
`,
{
variables: {
id: qrCode.productId,
},
}
);
const {
data: { product },
} = await response.json();
return {
...qrCode,
productDeleted: !product?.title,
productTitle: product?.title,
productImage: product?.media?.nodes[0]?.preview?.image?.url,
productAlt: product?.media?.nodes[0]?.preview?.image?.altText,
destinationUrl: getDestinationUrl(qrCode),
image: await qrCodeImagePromise,
};
}
export function validateQRCode(data) {
const errors = {};
if (!data.title) {
errors.title = "Title is required";
}
if (!data.productId) {
errors.productId = "Product is required";
}
if (!data.destination) {
errors.destination = "Destination is required";
}
if (Object.keys(errors).length) {
return errors;
}
}
```
### Validate QR codes
To create a valid QR code, the app user needs to provide a title, and select a product and destination. Add a function to ensure that, when the user submits the form to create a QR code, values exist for all of the required fields.
The action for the QR code form will return errors from this function.
## /app/models/QRCode.server.js
```javascript
import qrcode from "qrcode";
import invariant from "tiny-invariant";
import db from "../db.server";
export async function getQRCode(id, graphql) {
const qrCode = await db.qRCode.findFirst({ where: { id } });
if (!qrCode) {
return null;
}
return supplementQRCode(qrCode, graphql);
}
export async function getQRCodes(shop, graphql) {
const qrCodes = await db.qRCode.findMany({
where: { shop },
orderBy: { id: "desc" },
});
if (qrCodes.length === 0) return [];
return Promise.all(
qrCodes.map((qrCode) => supplementQRCode(qrCode, graphql))
);
}
export function getQRCodeImage(id) {
const url = new URL(`/qrcodes/${id}/scan`, process.env.SHOPIFY_APP_URL);
return qrcode.toDataURL(url.href);
}
export function getDestinationUrl(qrCode) {
if (qrCode.destination === "product") {
return `https://${qrCode.shop}/products/${qrCode.productHandle}`;
}
const match = /gid:\/\/shopify\/ProductVariant\/([0-9]+)/.exec(qrCode.productVariantId);
invariant(match, "Unrecognized product variant ID");
return `https://${qrCode.shop}/cart/${match[1]}:1`;
}
async function supplementQRCode(qrCode, graphql) {
const qrCodeImagePromise = getQRCodeImage(qrCode.id);
const response = await graphql(
`
query supplementQRCode($id: ID!) {
product(id: $id) {
title
media(first: 1) {
nodes {
preview {
image {
altText
url
}
}
}
}
}
}
`,
{
variables: {
id: qrCode.productId,
},
}
);
const {
data: { product },
} = await response.json();
return {
...qrCode,
productDeleted: !product?.title,
productTitle: product?.title,
productImage: product?.media?.nodes[0]?.preview?.image?.url,
productAlt: product?.media?.nodes[0]?.preview?.image?.altText,
destinationUrl: getDestinationUrl(qrCode),
image: await qrCodeImagePromise,
};
}
export function validateQRCode(data) {
const errors = {};
if (!data.title) {
errors.title = "Title is required";
}
if (!data.productId) {
errors.productId = "Product is required";
}
if (!data.destination) {
errors.destination = "Destination is required";
}
if (Object.keys(errors).length) {
return errors;
}
}
```
## Create a QR code form
Create a form that allows the app user to manage QR codes.
To create this form, you'll use a [Route module](https://reactrouter.com/start/framework/route-module), [Polaris web components](https://shopify.dev/docs/api/app-home/polaris-web-components) and [App Bridge](https://shopify.dev/docs/api/app-bridge).
### Set up the form route
Create a form that can create, update or delete a QR code.
In the `app` > `routes` folder, create a new file called `app.qrcodes.$id.jsx`.
***
#### Dynamic segments
This route uses a [dynamic segment route](https://reactrouter.com/start/framework/routing#dynamic-segments) to match the URL for creating a new QR code and editing an existing one.
If the user is creating a QR code, the URL is `/app/qrcodes/new`. If the user is updating a QR code, the URL is `/app/qrcodes/1`, where `1` is the ID of the QR code that the user is updating.
#### Remix layouts
The React Router template includes a [layout](https://reactrouter.com/start/framework/routing#layout-routes) at `app/routes/app.jsx`. This layout should be used for authenticated routes that render inside the Shopify admin. It's responsible for configuring App Bridge and Polaris web components, and authenticating the user using [shopify-app-react-router](https://www.npmjs.com/package/@shopify/shopify-app-react-router).
***
[App Bridge](https://shopify.dev/docs/api/app-bridge)[Polaris web components](https://shopify.dev/docs/api/app-home/polaris-web-components)
### Authenticate the user
Authenticate the route using `shopify-app-react-router`.
***
If the user isn't authenticated, `authenticate.admin` handles the necessary redirects. If the user is authenticated, then the method returns an admin object.
You can use the `authenticate.admin` method for the following purposes:
* Getting information from the session, such as the `shop`
* Accessing the [GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql)
* Within methods to require and request billing
***
[Authenticating admin requests](https://shopify.dev/docs/api/shopify-app-react-router/v0/authenticate/admin)
[](https://shopify.dev/docs/api/admin-graphql)
[GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql)
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Return a JSON Response
Return JSON data that can be used to populate the QR code form.
If the `id` parameter is `new`, return JSON with an empty title, and product for the destination. If the `id` parameter isn't `new`, then return the JSON from `getQRCode` to populate the QR code state.
***
[](https://shopify.dev/docs/api/admin-graphql)
[GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql)
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Manage the form state
Maintain the state of the QR code form state using the following variables:
* `initialFormState`: The initial state of the form. This only changes when the user submits the form. This state is copied from `useLoaderData` into React state.
* `formState`: When the user changes the title, selects a product, or changes the destination, this state is updated. This state is copied from `useLoaderData` into React state.
* `errors`: If the app user doesn't fill all of the QR code form fields, then the action returns errors to display. This is the return value of `validateQRCode`, which is accessed through the `useActionData` hook.
* `isSaving`: Keeps track of the network state using `useNavigation`. This state is used to disable buttons and show loading states.
* `isDirty`: Determines if the form has changed. This is used to enable save buttons when the app user has changed the form contents, or disable them when the form contents haven't changed.
***
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Add a product selector
Using the App Bridge `ResourcePicker` action, add a modal that allows the user to select a product. Save the selection to form state.

***
[ResourcePicker](https://shopify.dev/docs/api/app-home/apis/resource-picker)
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Submit
Use `useSubmit` to add the ability to save and delete a QR Code.
***
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Lay out the form
Using Polaris web components, build the layout for the form. Use `s-page`, `s-section`, and `s-box` with `slot="aside"` to structure the page. The page should have two columns.
***
Polaris is the design system for the Shopify admin. Using Polaris web components ensures that your UI is accessible, responsive, and displays consistently with the Shopify Admin.
[Polaris web components](https://shopify.dev/docs/api/app-home/polaris-web-components)[s-page](https://shopify.dev/docs/api/app-home/polaris-web-components/structure/page)[s-section](https://shopify.dev/docs/api/app-home/polaris-web-components/structure/section)[s-box](https://shopify.dev/docs/api/app-home/polaris-web-components/structure/box)
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Add breadcrumbs
Use an App Bridge `s-page` component to display a title that indicates to the user whether they're creating or editing a QR code. Include a breadcrumb link to go back to the [QR code list](#list-qr-codes).
***
[Title Bar](https://shopify.dev/docs/api/app-home/app-bridge-web-components/title-bar)
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Add a title field
Use `s-text-field` for updating the title. It should `setFormState`, have some `details` and show title errors from `useActionData`.
***
[s-text-field](https://shopify.dev/docs/api/app-home/polaris-web-components/forms/textfield)
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Add a way to select the product
If the user hasn't selected a product, then display a `s-button` with an `onClick` for `selectProduct`.
If the user has selected a product, use `s-image` to display the product image. Use `s-clickable`, `s-box`, `s-image` and `s-icon` to display the product image. Use `s-box` and `s-stack` to layout the UI.
***
[s-button](https://shopify.dev/docs/api/app-home/polaris-web-components/actions/button)[s-image](https://shopify.dev/docs/api/app-home/polaris-web-components/images/image)[s-clickable](https://shopify.dev/docs/api/app-home/polaris-web-components/actions/clickable)[s-image](https://shopify.dev/docs/api/app-home/polaris-web-components/media/image)[s-icon](https://shopify.dev/docs/api/app-home/polaris-web-components/media/icon)[s-box](https://shopify.dev/docs/api/app-home/polaris-web-components/structure/box)[s-stack](https://shopify.dev/docs/api/app-home/polaris-web-components/structure/box)
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Add destination options
Use `s-select` to render different destinations. It should `setFormState` when `onChange` occurs.
If the user is editing a QR code, use a `s-link` to link to the destination URL in a new tab.
***
[s-select](https://shopify.dev/docs/api/app-home/polaris-web-components/forms/select)[s-link](https://shopify.dev/docs/api/app-home/polaris-web-components/actions/link)
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Display a preview of the QR code
After saving a QR code, or when editing an existing QR code, provide ways to preview the QR code that the app user created.
Use `s-box` with `slot="aside"` to position the preview as an aside.
If a QR code is available, then use `s-image` to render the QR code. If no QR code is available, then use `s-text` with `color="subdued"`.
Add buttons to preview the public URL, and to download the QR code
***
[s-box](https://shopify.dev/docs/api/app-home/polaris-web-components/structure/box)[s-image](https://shopify.dev/docs/api/app-home/polaris-web-components/images/image)[s-text](https://shopify.dev/docs/api/app-home/polaris-web-components/typography/text)[s-button](https://shopify.dev/docs/api/app-home/polaris-web-components/actions/button)
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Add save bar
Use `shopify.saveBar` and `ui-save-bar` to render **Save** and **Discard** buttons.
Use the `useSubmit` hook to save the form data.
Copy the data that Prisma needs from `formState` and set the `cleanFormState` to the current `formState`.
***
[App Bridge save bar](https://shopify.dev/docs/api/app-home/app-bridge-web-components/ui-save-bar)
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
### Create, update, or delete a QR code
Create an `action` to create, update, or delete a QR code.
The `action` should use the store from the session. This ensures that the app user can only create, update, or delete QR codes for their own store.
The action should return errors for incomplete data using your `validateQRCode` function.
If the action deletes a QR code, redirect the app user to the index page. If the action creates a QR code, redirect to `app/qrcodes/$id`, where `$id` is the ID of the newly created QR code.
***
## /app/routes/app.qrcodes.$id.jsx
```jsx
import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
Link,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
```
## List QR codes
To allow app users to navigate to QR codes, list the QR codes in the app home.
### Load QR codes
In the app's index route, load the QR codes using a `loader`.
The `loader` should load QR codes using the `qrcodes` function from [`app/models/QRCode.server.js`](#get-qr-code-and-product-data), and return them.
***
## /app/routes/app.\_index.jsx
```jsx
import { useLoaderData, Link } from "react-router";
import { boundary } from "@shopify/shopify-app-react-router/server";
import { authenticate } from "../shopify.server";
import { getQRCodes } from "../models/QRCode.server";
export async function loader({ request }) {
const { admin, session } = await authenticate.admin(request);
const qrCodes = await getQRCodes(session.shop, admin.graphql);
return {
qrCodes,
};
}
const EmptyQRCodeState = () => (