I’m facing a scenario that I can’t solve.
I’m working on a NextJS 14 application, that is querying data from an external GraphQL API.
I have a Customers page, that display all of our customers.
In order to fetch our Customers, I’m using a server action get-customers.ts
:
"use server";
import { RequestResult } from "@/common/types/request.interface";
import { request } from "@/common/utils/http/request";
import {
CustomersDocument,
CustomersQuery,
CustomersQueryVariables,
} from "@/graphql/generated/graphql";
export default async function getCustomers(
variables: CustomersQueryVariables
): Promise<RequestResult<CustomersQuery>> {
console.log("About to query customers");
const { result }: { result: RequestResult<CustomersQuery> } = await request<
CustomersQuery,
CustomersQueryVariables
>(CustomersDocument, variables, ["customers"]);
return result;
}
In this server action, I’m calling request, which is my helper for fetching, and passing it some tags.
Then I have my page. In order to use my external API’s pagination, filtering and sorting feature, I have to re-call my query when my filters change, so I’m using useEffect
, and therefore I need to declare my page as a "use client"
page.
Here is my page (with truncated useless code) :
"use client";
export default function Customers() {
const [filters, setFilters] = useState<FilterInput>({
search: "",
orderBy: "id",
sortDirection: "desc",
});
const [paginationInput, setPaginationInput] = useState<PaginationInput>({
page: 1,
perPage: 10,
});
const handleFilterChange = useCallback((newFilters: FilterInput) => {
setFilters((prev) => ({ ...prev, ...newFilters }));
}, []);
const handlePageChange = useCallback((newPage: number) => {
setPaginationInput((prev) => ({ ...prev, page: newPage }));
}, []);
const [data, setData] = useState<CustomersQuery | null>(null);
const [errors, setErrors] = useState<AppError[] | null>(null);
useEffect(() => {
const fetchData = async () => {
const result: RequestResult<CustomersQuery> = await getCustomers({
...filters,
...paginationInput,
});
setData(result.data);
setErrors(result.errors ?? null);
};
fetchData();
}, [filters, paginationInput]);
if (errors) return <div>An error occured !</div>;
if (!data) return <div>Loading...</div>;
return (
<div className="grow flex flex-col gap-6">
<CreateCustomerSheet />
<div className="flex gap-2 items-center justify-between">
<Input
placeholder="Filter customers..."
value={filters.search?.toString()}
onChange={(event) => {
setPaginationInput((prev) => ({ ...prev, page: 1 }));
return handleFilterChange({ search: event.target.value });
}}
className="max-w-sm bg-background"
/>
<CreateCustomerSheet />
</div>
<DataTable<Partial<CustomerType>>
columns={columns}
data={data.customers.data}
paginationInput={paginationInput}
paginationInfo={data.customers.paginationInfo}
onPageChange={handlePageChange}
filterInput={filters}
onSortingChange={handleFilterChange}
/>
</div>
);
}
As you can see, this page is using a lot of client stuff.
Now, in my <CreateCustomerSheet />
, there is a form, that is calling another server action that creates new customers :
export default async function createCustomer(
_prevState: FormState,
data: FormData
): Promise<any> {
try {
const formData = Object.fromEntries(data);
const parsed = schema.safeParse(formData);
console.log("Parsed", parsed);
if (!parsed.success) {
let errors = parsed.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
}));
return { errors };
}
const { result } = await request<
CreateCustomerMutation,
CreateCustomerMutationVariables
>(CreateCustomerDocument, {
company_name: parsed.data.company_name,
email: parsed.data.email,
address1: parsed.data.address1,
city: parsed.data.city,
country: parsed.data.country,
});
if (result.data?.createCustomer) {
console.log("revalidating tag");
revalidateTag("/customers");
}
// if (result.errors) return { message: result.errors[0].message };
} catch (error) {
return { message: createAppError("UNKNOWN_ERROR").message };
}
}
When I create a new customer, I can see my logs in my server, telling me that creation is successfull, that it is revalidating tag, but my query is not re-triggered.
What did I try ?
I tried removing all the client stuff in my page.tsx
component in order to make it become a server component. Then when creating customer, I can clearly see my data getting updated.
What should I do ?
How could I make it a server component but keeping track of my user input when filtering ? I don’t understand how I could handle it.
In many examples I can find people querying data from server component before to pass it down to a client component, but I can’t manage to handle this case where I need my filters, pagination and sorting to update when I’m clicking on my datatable or filling my searchbar.
2
Answers
I came to a solution using URLSearchParams.
page.tsx
is a server component, getting searchParams :Then CustomersList is declared as a
"use client"
component, and updates the url params :This way filtering still works perfectly, and the page component being a server component, when calling
revalidateTag("customers")
, data is being updated automaticallyYou can make your
page.tsx
a server component, and wrap your logic in a client component, and then import it in the page:this way whenever you revalidateTag is called the data is refetched from
page.tsx
, so it should be updated.