DEV Community

Mohammed Shaheem P
Mohammed Shaheem P

Posted on

Shopify Orders, Setup Pagination with shopify-api and GraphQL

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); } } 
Enter fullscreen mode Exit fullscreen mode

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>  ); } 
Enter fullscreen mode Exit fullscreen mode
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>  ); } 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)