Building single-click app frontends 

BigCommerce apps built with Gadget have React frontends that have the BigDesign library pre-installed. These frontends are:

Your frontend files are in the web/ directory of your Gadget app.

Gadget has authorization built-in, so frontend app users will not be able to call actions until they are granted permission. You can manage API and data permissions in accessControl/permissions. Read more on frontend data security and access control below.

For more general information on building frontends with Gadget, see the frontend guide.

Getting started with single-click frontends 

After setting up a BigCommerce connection, your web/ directory will contain the following files:

File pathDescription
web/api.jsAn API client unique to your Gadget project and used to call your backend.
web/main.jsxThe entry point for your frontend project, sets up the BigDesign ThemeProvider.
web/components/App.jsxSet up routing and authenticates API client using Provider from @gadgetinc/react-bigcommerce.
web/routes/index.jsxThe default route that renders the frontend you see in a store.

You can start to make edits right away in web/routes/index.jsx and hot module reloading (HMR) will update your frontend in realtime.

Reading data from your backend 

You can use the API client in web/api.js along with React hooks from the @gadgetinc/react package to call your backend actions or HTTP routes, which reads data from your database.

If you need data that is not being stored in your database, you can make requests to the BigCommerce API from an action. We do not recommend making requests to the BigCommerce API directly from your frontend.

Using @gadgetinc/react hooks to read data 

The @gadgetinc/react package provides hooks to call your backend actions or HTTP routes. You can use these hooks to read data from your database. These hooks provide a simple, React-ful way to interact with your API and provide additional functionality like:

  • loading/fetching state,
  • error handling,
  • pagination, filtering, sorting, and searching,
  • and realtime queries

As an example, this is how you could read a list of products from your database using a useFindMany hook. This will fetch the first 50 products from the database and display them in a table, with a loading spinner while the request is fetching data. An error will also be displayed if there is an issue reading data:

web/routes/index.jsx
React
1import { Panel, Text, Flex, StatefulTable, ProgressCircle } from "@bigcommerce/big-design";
2import { ErrorIcon } from "@bigcommerce/big-design-icons";
3import { useFindMany } from "@gadgetinc/react";
4import { api } from "../api";
5
6// simple currency formatter
7let CAD = new Intl.NumberFormat("en-CA", {
8 style: "currency",
9 currency: "CAD",
10});
11
12export default function () {
13 // fetch the first 50 products from the database
14 const [{ data, fetching, error }] = useFindMany(api.bigcommerce.product);
15
16 // add an error message to the Panel if there is a request error
17 return (
18 <Panel
19 title="Read data example"
20 description={
21 error ? (
22 <Flex flexGap={"8px"}>
23 <ErrorIcon color="danger" /> <Text>Error: {error.message}</Text>
24 </Flex>
25 ) : undefined
26 }
27 >
28 {/** render a loading spinner while the request is fetching data */}
29 {fetching && <ProgressCircle size="large" />}
30 {/** draw a paginated product table with the returned data */}
31 {data?.length ? (
32 <StatefulTable
33 pagination
34 columns={[
35 { header: "Name", hash: "name", render: ({ name }) => name },
36 {
37 header: "Price",
38 hash: "price",
39 render: ({ price }) => (price ? CAD.format(price) : "No price"),
40 },
41 ]}
42 items={data}
43 />
44 ) : (
45 <Text>No product data in database!</Text>
46 )}
47 </Panel>
48 );
49}
1import { Panel, Text, Flex, StatefulTable, ProgressCircle } from "@bigcommerce/big-design";
2import { ErrorIcon } from "@bigcommerce/big-design-icons";
3import { useFindMany } from "@gadgetinc/react";
4import { api } from "../api";
5
6// simple currency formatter
7let CAD = new Intl.NumberFormat("en-CA", {
8 style: "currency",
9 currency: "CAD",
10});
11
12export default function () {
13 // fetch the first 50 products from the database
14 const [{ data, fetching, error }] = useFindMany(api.bigcommerce.product);
15
16 // add an error message to the Panel if there is a request error
17 return (
18 <Panel
19 title="Read data example"
20 description={
21 error ? (
22 <Flex flexGap={"8px"}>
23 <ErrorIcon color="danger" /> <Text>Error: {error.message}</Text>
24 </Flex>
25 ) : undefined
26 }
27 >
28 {/** render a loading spinner while the request is fetching data */}
29 {fetching && <ProgressCircle size="large" />}
30 {/** draw a paginated product table with the returned data */}
31 {data?.length ? (
32 <StatefulTable
33 pagination
34 columns={[
35 { header: "Name", hash: "name", render: ({ name }) => name },
36 {
37 header: "Price",
38 hash: "price",
39 render: ({ price }) => (price ? CAD.format(price) : "No price"),
40 },
41 ]}
42 items={data}
43 />
44 ) : (
45 <Text>No product data in database!</Text>
46 )}
47 </Panel>
48 );
49}

Docs on pagination, filtering, sorting, and searching on read can be found in your your API reference.

Want your frontend to stay up to date with your database?

Your Gadget API supports realtime queries out of the box. Using realtime queries will keep your frontend up to date with your database without needing to poll your backend.

Read about realtime queries here.

Reading data from BigCommerce 

If you want to read data that is not stored in your Gadget database, you can make requests to the BigCommerce API from your backend action. This is the recommended way to read data from BigCommerce in your frontend.

Here is an example of how you could read product data from BigCommerce in a global action, call that action from your frontend in a useEffect hook, and display the returned data in a table.

First, the global action:

api/actions/bigcommerce/readProducts.js
JavaScript
1export const run: ActionRun = async ({ params, logger, api, connections }) => {
2 // BigCommerce API client
3 const bigcommerce = connections.bigcommerce.current;
4 // use the API client to fetch 5 products, and return
5 return await bigcommerce?.v3.get(`/catalog/products`, { query: { limit: 5 } });
6};
1export const run: ActionRun = async ({ params, logger, api, connections }) => {
2 // BigCommerce API client
3 const bigcommerce = connections.bigcommerce.current;
4 // use the API client to fetch 5 products, and return
5 return await bigcommerce?.v3.get(`/catalog/products`, { query: { limit: 5 } });
6};

And here is how you can call that global action from your frontend using the useGlobalAction hook. This will fetch the first 5 products from BigCommerce and display them in a table, with a loading spinner while the request is fetching data. An error will also be displayed if there is an issue calling the action:

web/routes/index.jsx
React
1import { Panel, Text, Flex, StatefulTable, ProgressCircle } from "@bigcommerce/big-design";
2import { ErrorIcon } from "@bigcommerce/big-design-icons";
3import { useGlobalAction } from "@gadgetinc/react";
4import { api } from "../api";
5import { useEffect } from "react";
6
7// simple currency formatter
8let CAD = new Intl.NumberFormat("en-CA", {
9 style: "currency",
10 currency: "CAD",
11});
12
13export default function () {
14 // use the useGlobalAction hook to call a global action
15 const [{ data, fetching, error }, readProducts] = useGlobalAction(api.bigcommerce.readProducts);
16
17 useEffect(() => {
18 // call the function provided by the hook that runs the action
19 readProducts();
20 }, []);
21
22 // add an error message to the Panel if there is a request error
23 return (
24 <Panel
25 title="Read data directly from BigCommerce example"
26 description={
27 error ? (
28 <Flex flexGap={"8px"}>
29 <ErrorIcon color="danger" /> <Text>Error: {error.message}</Text>
30 </Flex>
31 ) : undefined
32 }
33 >
34 {/** render a loading spinner while the request is fetching data */}
35 {fetching && <ProgressCircle size="large" />}
36 {/** draw a paginated product table with the returned data */}
37 {data?.length ? (
38 <StatefulTable
39 pagination
40 columns={[
41 { header: "Name", hash: "name", render: ({ name }) => name },
42 { header: "Price", hash: "price", render: ({ price }) => CAD.format(price) },
43 ]}
44 items={data}
45 />
46 ) : (
47 <Text>No product data found!</Text>
48 )}
49 </Panel>
50 );
51}
1import { Panel, Text, Flex, StatefulTable, ProgressCircle } from "@bigcommerce/big-design";
2import { ErrorIcon } from "@bigcommerce/big-design-icons";
3import { useGlobalAction } from "@gadgetinc/react";
4import { api } from "../api";
5import { useEffect } from "react";
6
7// simple currency formatter
8let CAD = new Intl.NumberFormat("en-CA", {
9 style: "currency",
10 currency: "CAD",
11});
12
13export default function () {
14 // use the useGlobalAction hook to call a global action
15 const [{ data, fetching, error }, readProducts] = useGlobalAction(api.bigcommerce.readProducts);
16
17 useEffect(() => {
18 // call the function provided by the hook that runs the action
19 readProducts();
20 }, []);
21
22 // add an error message to the Panel if there is a request error
23 return (
24 <Panel
25 title="Read data directly from BigCommerce example"
26 description={
27 error ? (
28 <Flex flexGap={"8px"}>
29 <ErrorIcon color="danger" /> <Text>Error: {error.message}</Text>
30 </Flex>
31 ) : undefined
32 }
33 >
34 {/** render a loading spinner while the request is fetching data */}
35 {fetching && <ProgressCircle size="large" />}
36 {/** draw a paginated product table with the returned data */}
37 {data?.length ? (
38 <StatefulTable
39 pagination
40 columns={[
41 { header: "Name", hash: "name", render: ({ name }) => name },
42 { header: "Price", hash: "price", render: ({ price }) => CAD.format(price) },
43 ]}
44 items={data}
45 />
46 ) : (
47 <Text>No product data found!</Text>
48 )}
49 </Panel>
50 );
51}

You will also have to grant the bigcommerce-app-users role access to the bigcommerce/readProducts action in accessControl/permissions!

Writing data to your database 

To write data to your database, you can make use of the useAction, useGlobalAction, or useActionForm hooks from the @gadgetinc/react package. These hooks allow you to call your backend actions from your frontend.

Simple example with useAction 

For example, if you wanted to save a record to a gizmo model when a button is clicked, you could use the useAction hook to call the gizmo.create action:

simple useAction hook to call a model action
JavaScript
1import { Panel, Button, Box } from "@bigcommerce/big-design";
2import { useAction } from "@gadgetinc/react";
3import { api } from "../api";
4
5export default function () {
6 // use the useAction hook to call a global action
7 const [{ fetching: savingGizmo }, createGizmo] = useAction(api.gizmo.create);
8
9 // add an error message to the Panel if there is a request error
10 return (
11 <Panel title="Write data example">
12 <Box marginTop="xxLarge">
13 <Button onClick={async () => await createGizmo()} disabled={savingGizmo}>
14 Add keyword
15 </Button>
16 </Box>
17 </Panel>
18 );
19}
1import { Panel, Button, Box } from "@bigcommerce/big-design";
2import { useAction } from "@gadgetinc/react";
3import { api } from "../api";
4
5export default function () {
6 // use the useAction hook to call a global action
7 const [{ fetching: savingGizmo }, createGizmo] = useAction(api.gizmo.create);
8
9 // add an error message to the Panel if there is a request error
10 return (
11 <Panel title="Write data example">
12 <Box marginTop="xxLarge">
13 <Button onClick={async () => await createGizmo()} disabled={savingGizmo}>
14 Add keyword
15 </Button>
16 </Box>
17 </Panel>
18 );
19}

Writing data back to BigCommerce 

If you want to write data back to BigCommerce, you can use the BigCommerce API client in your backend action. This is the recommended way to write data back to BigCommerce in your frontend.

Here is an example of how you could write product data to BigCommerce in a global action, and then call that action from your frontend using useGlobalAction. First, the action:

api/actions/createMetafield.js
JavaScript
1export const run: ActionRun = async ({ params, logger, api, connections }) => {
2 // set up BigCommerce client
3 const bigcommerce = connections.bigcommerce.current;
4 if (!bigcommerce) {
5 throw new Error("Missing bigcommerce connection");
6 }
7
8 if (!params.value || !params.productId) {
9 throw new Error(
10 "The `value` and `productId` parameters are required to run this action"
11 );
12 }
13
14 // create a metafield using custom data from the frontend
15 await bigcommerce.v3.post("/catalog/products/{product_id}/metafields", {
16 path: { product_id: params.productId },
17 body: {
18 permission_set: "app_only",
19 namespace: "metafield-namespace",
20 key: "metafield-key",
21 value: params.value,
22 description: "New metafield description",
23 },
24 });
25};
26
27// define custom params for the action
28export const params = {
29 productId: { type: "number" },
30 value: { type: "string" },
31};
1export const run: ActionRun = async ({ params, logger, api, connections }) => {
2 // set up BigCommerce client
3 const bigcommerce = connections.bigcommerce.current;
4 if (!bigcommerce) {
5 throw new Error("Missing bigcommerce connection");
6 }
7
8 if (!params.value || !params.productId) {
9 throw new Error(
10 "The `value` and `productId` parameters are required to run this action"
11 );
12 }
13
14 // create a metafield using custom data from the frontend
15 await bigcommerce.v3.post("/catalog/products/{product_id}/metafields", {
16 path: { product_id: params.productId },
17 body: {
18 permission_set: "app_only",
19 namespace: "metafield-namespace",
20 key: "metafield-key",
21 value: params.value,
22 description: "New metafield description",
23 },
24 });
25};
26
27// define custom params for the action
28export const params = {
29 productId: { type: "number" },
30 value: { type: "string" },
31};

And here is how you can call that global action, passing in values for the custom productId and value params, from your frontend using the useGlobalAction hook:

web/routes/index.jsx
React
1import { Panel, Form, FormGroup, Input, Button, Box } from "@bigcommerce/big-design";
2import { useGlobalAction } from "@gadgetinc/react";
3import { api } from "../api";
4import { useState } from "react";
5
6export default function () {
7 const [value, setValue] = useState("");
8
9 // use the useGlobalAction hook to call the createMetafield global action
10 const [{ fetching }, createMetafield] = useGlobalAction(api.bigcommerce.createMetafield);
11
12 return (
13 <Panel title="Read data example">
14 <Form
15 onSubmit={async (event) => {
16 event.preventDefault();
17 await createMetafield({ productId: 1, value });
18 }}
19 >
20 <FormGroup>
21 <Input
22 description="Please enter a metafield value."
23 label="Metafield value"
24 required
25 value={value}
26 onChange={(event) => setValue(event.target.value)}
27 disabled={fetching}
28 />
29 </FormGroup>
30 <Box marginTop="xxLarge">
31 <Button type="submit" disabled={fetching}>
32 Create metafield
33 </Button>
34 </Box>
35 </Form>
36 </Panel>
37 );
38}
1import { Panel, Form, FormGroup, Input, Button, Box } from "@bigcommerce/big-design";
2import { useGlobalAction } from "@gadgetinc/react";
3import { api } from "../api";
4import { useState } from "react";
5
6export default function () {
7 const [value, setValue] = useState("");
8
9 // use the useGlobalAction hook to call the createMetafield global action
10 const [{ fetching }, createMetafield] = useGlobalAction(api.bigcommerce.createMetafield);
11
12 return (
13 <Panel title="Read data example">
14 <Form
15 onSubmit={async (event) => {
16 event.preventDefault();
17 await createMetafield({ productId: 1, value });
18 }}
19 >
20 <FormGroup>
21 <Input
22 description="Please enter a metafield value."
23 label="Metafield value"
24 required
25 value={value}
26 onChange={(event) => setValue(event.target.value)}
27 disabled={fetching}
28 />
29 </FormGroup>
30 <Box marginTop="xxLarge">
31 <Button type="submit" disabled={fetching}>
32 Create metafield
33 </Button>
34 </Box>
35 </Form>
36 </Panel>
37 );
38}

This pattern allows you to send data to BigCommerce on a frontend action. Firing an action doesn't need to use a form! You can use any event to trigger an action, like a button click.

Building forms 

If you are building a form, you can use the useActionForm hook from the @gadgetinc/react package. This hook simplifies form handling by providing a way to handle form state, validation, and submission.

The useActionForm hook wraps the react-hook-form library and works nicely with BigDesign. Here is an example of how you could use useActionForm to create a form that writes data to a manufacturer model:

web/routes/index.jsx
React
1import { Panel, Form, FormGroup, Input, Button, Box } from "@bigcommerce/big-design";
2import { useActionForm } from "@gadgetinc/react";
3import { api } from "../api";
4
5export default function () {
6 // useActionForm calls the manufacturer.create action on submit
7 const { submit, register } = useActionForm(api.bigcommerce.manufacturer.create);
8
9 // add an error message to the Panel if there is a request error
10 return (
11 <Panel title="Simple form example">
12 <Form onSubmit={submit}>
13 <FormGroup>
14 {/** register is used to associate the Input component with the "name" field on the model */}
15 <Input description="Enter the manufacturer name." label="Manufacturer name" required {...register("name")} />
16 </FormGroup>
17 <Box marginTop="xxLarge">
18 <Button type="submit">Create manufacturer</Button>
19 </Box>
20 </Form>
21 </Panel>
22 );
23}
1import { Panel, Form, FormGroup, Input, Button, Box } from "@bigcommerce/big-design";
2import { useActionForm } from "@gadgetinc/react";
3import { api } from "../api";
4
5export default function () {
6 // useActionForm calls the manufacturer.create action on submit
7 const { submit, register } = useActionForm(api.bigcommerce.manufacturer.create);
8
9 // add an error message to the Panel if there is a request error
10 return (
11 <Panel title="Simple form example">
12 <Form onSubmit={submit}>
13 <FormGroup>
14 {/** register is used to associate the Input component with the "name" field on the model */}
15 <Input description="Enter the manufacturer name." label="Manufacturer name" required {...register("name")} />
16 </FormGroup>
17 <Box marginTop="xxLarge">
18 <Button type="submit">Create manufacturer</Button>
19 </Box>
20 </Form>
21 </Panel>
22 );
23}

Notice how there are no useState hooks managing form data. The useActionForm hook handles all of that for you!

For more information on building forms and additional examples, see the forms guide.

Working with useActionForm and BigDesign

The BigDesign component library extends the native HTML form elements with additional props. This is great news when working with useActionForm, as all of the BigDesign components are compatible with the register function provided by useActionForm.

The register function is used to associate the BigDesign component with the field on the model, handles form state, and allows you to add custom frontend validation.

Calling HTTP routes 

There are cases where you need to use an HTTP route instead of an action, for example, when you need to stream a response back to your frontend.

The @gadgetinc/react package has a useFetch hook for calling HTTP routes, or you can use api.fetch() built into the Gadget API client.

More information on calling HTTP routes from your frontend can be found in the calling HTTP routes guide.

Calling the BigCommerce API from an HTTP route 

When you make a request to a BigCommerce API from an HTTP route, you will need to make sure the instance of the Gadget API client in your route is properly authenticated. You can use connections.bigcommerce to create an instance of the BigCommerce API client, similar to actions.

Authenticated API clients 

If you use the useFetch() hook from within a single-click frontend, your API client will already be authenticated with the BigCommerce store. This means you can use connections.bigcommerce.current to create an instance of the BigCommerce API client in your HTTP route.

api/routes/GET-products.js
JavaScript
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply, connections }) => {
4 const bigcommerce = connections.bigcommerce.current;
5 const products = await bigcommerce?.v3.get(`/catalog/products`, {
6 query: { limit: 5 },
7 });
8 await reply.send({
9 products,
10 });
11};
12
13export default route;
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply, connections }) => {
4 const bigcommerce = connections.bigcommerce.current;
5 const products = await bigcommerce?.v3.get(`/catalog/products`, {
6 query: { limit: 5 },
7 });
8 await reply.send({
9 products,
10 });
11};
12
13export default route;

Unauthenticated API clients 

If you are calling your HTTP route from an external source, you will need to authenticate the API client with the store hash. You can use connections.bigcommerce.forStoreHash() to create an instance of the BigCommerce API client in your HTTP route.

api/routes/GET-products.js
JavaScript
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply, connections }) => {
4 const bigcommerce = await connections.bigcommerce.forStoreHash(
5 "<add-your-store-hash>"
6 );
7 const products = await bigcommerce?.v3.get(`/catalog/products`, {
8 query: { limit: 5 },
9 });
10 await reply.send({
11 products,
12 });
13};
14
15export default route;
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply, connections }) => {
4 const bigcommerce = await connections.bigcommerce.forStoreHash(
5 "<add-your-store-hash>"
6 );
7 const products = await bigcommerce?.v3.get(`/catalog/products`, {
8 query: { limit: 5 },
9 });
10 await reply.send({
11 products,
12 });
13};
14
15export default route;

Frontend data security and access control 

Gadget also handles authorization and access control for your APIs. When you make a request to your backend, Gadget will check if the user has the correct permissions to access the data. If the user does not have the correct permissions, Gadget will return a 403 error.

You can see permissions for your app at accessControl/permissions.

BigCommerce merchants who interact with your app frontend are granted a bigcommerce-app-users role. By default, they will only have read and write access to the bigcommerce/store/ model's actions.

For any additional model or global actions, you will need to manually grant the bigcommerce-app-users role access to those actions.

HTTP routes are not secure by default, so it is recommended to use actions unless you have a specific need for a route.

Granting permissions and data security

It is a good idea to only grant the permissions that are necessary for your app to function. This will help keep your app secure and prevent unauthorized access to your data.

If you are building a public single-click app, you need to handle multi-tenancy so stores cannot access each other's data. Read our multi-tenancy docs for more information.

session management 

The BigCommerce connection manages records of the backend session model when using @gadgetinc/react-bigcommerce. When a merchant loads a single-click frontend, the <Provider /> will:

  • handle the call to the /load endpoint
  • verify the jwt payload
  • ensure the user matches the store owner (when multiple users are not enabled), or manage the user session when multiple users are enabled

If the payload is valid, the connection will provision a new session record for the userId and store. Valid sessions will be granted the bigcommerce-app-users role, which is used to manage access to actions and models. This session is then passed to all your backend application's model filters and is available within actions.

Was this page helpful?