Add asc/desc sort into columns
When I set out to implement the ability to sort columns in asc/desc orders in the application, I anticipated a straightforward process. However, what seemed simple turned into a challenging endeavor that took me three days to complete.
Challenges Encountered:
- Initially, I believed it would involve simply adding a state variable in the
IssuesPage
to maintain the sorting order. However, I soon realized that useState
could only be used within client-side components. This means, IssuesPage
, which was a dynamically rendered server-side component, needs to become a client side component.
- Data fetching in the
IssuesPage
was initially directly performed using prisma
, bypassing the need for an API call. This required implementing a dedicated API to enable data retrieval.
- Navigation within the application was facilitated using
href
in the <Link>
component. However, after converting IssuePage
into a client-side component, I encountered an issue where the links updated but needed a manual refresh (by pressing the enter key in the URL bar) to re-render the page.
- I changed
searchParams
to states, switched to using useRouter
for navigation. and useQuery
for fetching data. However, despite all these changes, the IssuePage
failed to automatically refetch data upon changes in status and sort order. The switching to using stats and passing the params down the components especially made the code seem clunky.
- I also needed to figure out how to retrieve search parameters in the backend and validate them before using prisma to fetch data.
IssuesPage
Make searchParams a state variable
- Add
sort
to the interface
- Use the
interface searchparams
as the type of the state variable
- All the
undefined
seem clunky to me
"use client";
import { IssueToolBar } from "@/app/components";
import { Issue, Status } from "@prisma/client";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { useState } from "react";
import IssuesPageSkeleton from "./_components/IssuesPageSkeleton";
import IssuesTable from "./_components/IssuesTable";
interface searchParams {
status: Status | undefined;
orderBy: keyof Issue | undefined;
sort: "asc" | "desc";
}
const IssuesPage = () => {
const [searchParams, setSearchParams] = useState<searchParams>({
status: undefined,
orderBy: undefined,
sort: "asc",
});
The handle change functions
There are to be passed down to the table and filter components for them to change sort and filter parameters.
This was originally written as a single function, but split into two to reduce complexity.
const handleChangeStatus = ({ status }: { status: Status | undefined }) => {
setSearchParams((prevSearchParams) => ({
...prevSearchParams,
status,
}));
};
const handleChangeSort = ({
orderBy,
}: {
orderBy: keyof Issue | undefined;
}) => {
setSearchParams((prevSearchParams) => {
const newSortDir = prevSearchParams.sort === "asc" ? "desc" : "asc";
return {
...prevSearchParams,
orderBy,
sort: newSortDir,
};
});
};
The states and functions are passed down to the components.
In the case of handleChangeStatus
, the state and function need to be passed one layer further by <IssueToolBar>
to <IssueStatusFilter>
return (
<div>
<IssueToolBar
status={searchParams.status}
handleChangeStatus={handleChangeStatus}
/>
<IssuesTable
orderBy={searchParams.orderBy}
sort={searchParams.sort}
issues={issues}
handleChangeSort={handleChangeSort}
/>
</div>
);
IssueStatusFilter
I learned how to type define a function in the interface.
interface IssueToolBarProps {
status: Status | undefined;
handleChangeStatus: ({ status }: { status: Status | undefined }) => void;
}
const IssueStatusFilter = ({
status,
handleChangeStatus,
}: IssueToolBarProps) => {
const router = useRouter();
return (
<Select.Root
defaultValue={status}
onValueChange={(selectedStatus: "ALL" | Status) => {
const query: undefined | Status =
selectedStatus === "ALL" ? undefined : selectedStatus;
handleChangeStatus({status: query});
}}
>
IssueTable
Add conditionally render of the sort order arrow icons
interface IssuesTableProps {
orderBy: keyof Issue | undefined;
sort: "asc" | "desc";
issues: Issue[];
handleChangeSort: ({ orderBy }: { orderBy: keyof Issue | undefined }) => void;
}
export default function IssuesTable({
orderBy,
sort,
issues,
handleChangeSort,
}: IssuesTableProps) {
return (
<Table.Root variant="surface">
<Table.Header>
<Table.Row>
{columns.map((col) => (
<Table.ColumnHeaderCell key={col.value} className={col.className}>
<button
onClick={() => {
handleChangeSort({ orderBy: col.value });
}}
>
{col.label}
</button>
{/* Show an indicator if a column header is clicked */}
{col.value === orderBy &&
(sort === "asc" ? (
<ArrowUpIcon className="inline" />
) : (
<ArrowDownIcon className="inline" />
))}
</Table.ColumnHeaderCell>
))}
</Table.Row>
</Table.Header>
Refetch data on state change
I was expect a state varible change to trigger a re-fetching of the issues
and rerender of the IssuesPage
, however, it did not work.
I found out there is refetch
variable in useQuery
, and tried to use it in useEffect
, but could not get it working as I got a compliant saying useEffect
can not be ran conditionally.
When I did get refreshing data working (I can not remember how), the refetch was lagging one state behind the current. e.g. if I change filter to “OPEN”, I need to change it again to see the “OPEN” issues.
The solution is in fact simple. I just needed to add searchParams
into the queryKey
. I should spend more time learning useQuery
.
const {
isPending,
error,
data: issues,
} = useQuery<Issue[]>({
// By passing an array ["issues", searchParams] as the queryKey,
// the useQuery hook will properly identify changes in searchParams
// and trigger a refetch whenever searchParams change.
queryKey: ["issues", searchParams],
queryFn: () =>
axios
.get("/api/issues", {
params: searchParams,
})
.then((res) => res.data),
});
if (isPending) return <IssuesPageSkeleton />;
if (error) {
return;
}
Thoughts
I do not like the combersome passing of params down the components.
I wonder if it is still possible to get to passing the searchParams using href
. And if that is possible, would the change of searchParams
trigger useQuery
to re-fetch data? Perhaps yes, because the searchParams
will come in as params of IssuesPage
.
To be investigated.
GET Issues API
Retrieving search parameters
In POST and PATCH api calls, we use get the body of a request using
const body = await request.json()
where request: NextRequest
But this does not work for GET.
Instead, I need to use
export async function GET(request: NextRequest) {
// Extract search parameters
const searchParams = request.nextUrl.searchParams;
I can then destruct the searchParams
and use the variables for validation
const { status, orderBy, sort } = {
status: searchParams.get('status'),
orderBy: searchParams.get('orderBy'),
sort: searchParams.get('sort'),
};
// Valid status
const validation = getIssuesSchema.safeParse({
status,
orderBy,
sort,
});
if (!validation.success)
return NextResponse.json(
{ error: "Invalid key for sorting." },
{ status: 404 }
);
Validation schema
The schema is defined in validationSechme.ts
- Using
enum
and z.nativeEum
gets rid of type errors.
- Using
nullable
also helps with type errors
import { Status } from "@prisma/client";
// Validation schema for getting all issues
// [ 'OPEN', 'IN_PROGRESS', 'CLOSED' ]
enum orderByValues {
Title = 'title',
Status = 'status',
UpdatedAt = 'updatedAt',
}
enum sortOrders {
Asc = 'asc',
Desc = 'desc'
}
export const getIssuesSchema = z.object({
status: z.nativeEnum(Status).optional().nullable(),
orderBy: z.nativeEnum(orderByValues).optional().nullable(),
sort: z.nativeEnum(sortOrders).optional().nullable(),
})
Fetching data
I still have a type error with respect to where in the code below.
where
in the prisma
call should be of type IssueWhereInput | undefined
the where
I am passing in is of type {status: stirng} | {status?: undefined}
==How do I rectify this issue?==
// Validate and handle null or undefined values for status and orderBy
// Only add status condition if it exists
const where = status ? { status } : {};
// Only add orderBy if it exists
const orderByParam = orderBy ? { [orderBy]: sort } : undefined;
try {
const issues = await prisma.issue.findMany({
where,
orderBy: orderByParam,
});
return NextResponse.json(issues, { status: 200 });
} catch (error) {
console.log(error);
}