Skip to content

Shopify Orders, Setup Pagination with shopify-api and GraphQL

Published:

Table of contents

Open Table of contents

Introduction

I took more time than I should have taken while trying to build an orders page with pagination using a React and NestJS with typescript.

Between very few references and average documentations, I’m sure this article could be helpful to someone else who endup in this situation (or better I might endup losing this code and would need a place to find this), so why not write an article about it!

I was using NestJS for backend, but anyone using any other node framework could easily follow this, and I’m using Shopify Polaris for frontend components.

For the sake of simplicity, I’m only adding important Code Blocks here

Backend Function

pageSize = 25;

async fetchOrders(
  gqlClient: GraphqlClient,
  cursor: NullableString = null,
  direction: NullableString = FetchDirection.FORWARD,
) {
  const [first, last, after, before] =
    direction === FetchDirection.FORWARD
      ? [this.pageSize, null, cursor, null]
      : [null, this.pageSize, null, cursor];

  try {
    const response = await gqlClient.query<OrdersResponse>({
      data: {
        query: `query GetOrders($first: Int, $last: Int, $before: String, $after: String) {
          orders(first: $first, last: $last, before: $before, after: $after) {
            edges {
              cursor
              node {
                id
                name
                customer {
                  firstName
                  lastName
                }
                displayFinancialStatus
                displayFulfillmentStatus
                totalPriceSet {
                  presentmentMoney {
                    amount
                  }
                }
                createdAt
              }
            }
            pageInfo {
              hasNextPage
              hasPreviousPage
            }
          }
        }`,
        variables: {
          first,
          last,
          before,
          after,
        },
      },
    });
    return response.body.data.orders;
  } catch (error) {
    throw new InternalServerErrorException(error);
  }
}

Frontend Components

export default function OrdersPage() {
  const fetch = useAuthenticatedFetch();
  const [searchParams] = useSearchParams({});

  const [hasNextPage, sethasNextPage] = useState<boolean>(false);
  const [hasPreviousPage, setHasPreviousPage] = useState<boolean>(false);
  const [isLoading, setIsLoading] = useState<boolean>(true);
  const [orderEdges, setOrderEdges] = useState<OrderEdge[]>([]);

  const fetchOrders = useCallback(
    async (
      direction: NullableString = "forward",
      cursor: NullableString = "",
    ) => {
      setIsLoading(true);
      let fetchUrl;
      if (direction && cursor) {
        fetchUrl = `/api/orders/?cursor=${cursor}&direction=${direction}`;
      } else {
        fetchUrl = "/api/orders";
      }

      try {
        const { edges, pageInfo }: OrdersResponse = await fetch(fetchUrl, {
          method: "GET",
        }).then((res) => res.json());

        setOrderEdges(edges);

        sethasNextPage(pageInfo.hasNextPage);
        setHasPreviousPage(pageInfo.hasPreviousPage);
        setIsLoading(false);

        return;
      } catch (error) {
        console.error(error);
        setIsLoading(false);
        return;
      }
    },
    [],
  );

  useEffect(() => {
    const cursor = searchParams.get("cursor");
    const direction = searchParams.get("direction");

    fetchOrders(direction, cursor);
  }, [searchParams]);

  return (
    <Page>
      <TitleBar title="Orders" />
      <Layout>
        <Layout.Section>
          <OrdersTable
            orderEdges={orderEdges}
            hasNextPage={hasNextPage}
            hasPreviousPage={hasPreviousPage}
            loading={isLoading}
          />
        </Layout.Section>
      </Layout>
    </Page>
  );
}
const resourceName = {
  singular: "order",
  plural: "orders",
};

interface OrdersTableProps {
  orderEdges: OrderEdge[];
  hasNextPage: boolean;
  hasPreviousPage: boolean;
  loading: boolean;
}

export function OrdersTable({
  orderEdges,
  hasNextPage,
  hasPreviousPage,
  loading,
}: OrdersTableProps) {
  const [_, setSearchParams] = useSearchParams({});

  const resourceIDResolver = (order: OrderEdge) => {
    return order.node.id;
  };

  const { selectedResources, allResourcesSelected, handleSelectionChange } = useIndexResourceState(orderEdges, {
      resourceIDResolver,
    });

  const indexTableRowMarkup = useMemo(() => {
    return orderEdges.map(
      (
        {
          node: {
            id,
            name,
            createdAt,
            customer,
            displayFinancialStatus,
            displayFulfillmentStatus,
            totalPriceSet: {
              presentmentMoney: { amount },
            },
          },
        },
        index,
      ) => {
        const customerFullName =
          customer &&
          `${customer?.firstName || ""} ${customer?.lastName || ""}`.trim();
        const financialStatusForDisplay =displayFinancialStatus
                .split("_")
                .map((piece) => capitalize(piece))
                .join(" ");

        const financialStatusBadgeType = displayFinancialStatus === FinancialStatus.PAID
              ? "success"
              : undefined;
        const fulfillmentStatusBadgeType = displayFulfillmentStatus === FulfillmentStatus.FULFILLED
              ? "success"
              : undefined;

        return (
          <IndexTable.Row
            id={id}
            key={id}
            selected={selectedResources.includes(id)}
            position={index}
          >
            <IndexTable.Cell>
              <TextStyle variation="strong">{name}</TextStyle>
            </IndexTable.Cell>
            <IndexTable.Cell>
              {new Date(createdAt).toLocaleDateString()}
            </IndexTable.Cell>
            <IndexTable.Cell>{customerFullName}</IndexTable.Cell>
            <IndexTable.Cell>
                <Badge status={financialStatusBadgeType}>
                    {financialStatusForDisplay}
                </Badge>
            </IndexTable.Cell>
            <IndexTable.Cell>
              <Badge status={fulfillmentStatusBadgeType}>
                  {capitalize(
                      (displayFulfillmentStatus as FulfillmentStatus) ||
                        "unfulfilled",
                    )}
              </Badge>
            </IndexTable.Cell>
            <IndexTable.Cell>{amount}</IndexTable.Cell>
          </IndexTable.Row>
        );
      },
    );
  }, [orderEdges, selectedResources]);

  return (
    <Card sectioned>
      <IndexTable
        resourceName={resourceName}
        itemCount={orderEdges.length}
        selectedItemsCount={
          allResourcesSelected ? "All" : selectedResources.length
        }
        onSelectionChange={handleSelectionChange}
        headings={[
          { title: "Order" },
          { title: "Date" },
          { title: "Customer" },
          { title: "Payment Status" },
          { title: "Fulfillment Status" },
          { title: "Total" },
        ]}
        loading={loading}
      >
        {indexTableRowMarkup}
      </IndexTable>
      <div className={styles.paginationWrapper}>
        <Pagination
          hasPrevious={hasPreviousPage}
          hasNext={hasNextPage}
          onPrevious={() => {
            const cursor = orderEdges[0].cursor;
            setSearchParams({ cursor, direction: "backward" });
          }}
          onNext={() => {
            const cursor = orderEdges[orderEdges.length - 1].cursor;
            setSearchParams({ cursor, direction: "forward" });
          }}
        />
      </div>
    </Card>
  );
}
If you found this article helpful consider supporting me by Buying me a Coffee Buy Me A Coffee


Previous Post
How to Setup your MacBook Air M2 for Software Development