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>
);
}