Dynamic Filtering items with prisma and Nextjs?

Oi everyone! I use prisma in combination with Nextjs and mongodb on a project that deals with animal adoption. Namely, now I would like to do dynamic filtering by certain categories and enable it to interact with the user on the client side. Well, I'm interested in whether it's feasible to do it using prism, since I found out through the documentation that I can filter out certain things via the where: { } query. Any help and suggestions on how to do this are welcome. Thank you all!
50 Replies
James4u
James4u6mo ago
yeah, you can @tom_bombadil I can recommend best libraries for your case
James4u
James4u6mo ago
https://nuqs.47ng.com/ this library will be very helpful for you to store values for filtering in the url
nuqs | Type-safe search params state management for Next.js
Type-safe search params state management for Next.js. Like React.useState, but stored in the URL query string.
James4u
James4u6mo ago
and then in your server component you can use prisma to fetch your data while filtering out by query params in the url. let me know if you have any questions
tom_bombadil
tom_bombadilOP6mo ago
@James4u thank you for answer! Allow me to explain to you what the current situation is on my project, so you can perhaps explain to me 'closer' in which direction I should go. Actually, I have specifically 4 filters, i.e. 4 categories by which I want to filter the items from the array that I display, which are (category: Dog, Cat, Others; location: some cities; ages: young, adult; gender: female and male) . What I managed to do with prisma is that when I set it up in the function on the server side like this: export async function getAdoptPost( context: any, location?: string, ) { const page = parseInt((context.query.page as string) || "1", 12); const pageSize = 12; const total = await db.adoptAnimal.count({}); const post = await db.adoptAnimal.findMany({ skip: (page - 1) * pageSize, take: pageSize, where: { location: location, }, orderBy: { createdAt: "desc", }, }); return { post, page, pageSize, total, }; } this is an example where I only put 'location' as one of the possible options in the filter and now on the client side if I call this function 'getAdoptPost' and assign it 'location' something like this: const [location, setLocation] = useState<string>("Boston"); const fetchData = (page: number) => { startTransition(async () => { const result = await getAdoptPost({ query: { page }, location, }); setPosts(result.post); setPage(result.page); setPageSize(result.pageSize); setLocation(location); setTotal(result.total); setIsLoading(false); // Set loading to false after data is fetched console.log(result); console.log(location); }); }; because of this state, in which I hard-coded the location 'Boston', it will filter out all the animals that have that location on them, but I would like to make it so that the user selects that location via the filter below, and I get the results.
James4u
James4u6mo ago
are you doing client side data-fetching?
tom_bombadil
tom_bombadilOP6mo ago
that I also have a FIlterMenu component on the client side where there are fields with filters and through these fields I collect the data that the user picks up from the filter. And this is a function that is called on the submit FIlterMenu and I put all those filters that the user selects in the 'allFilters' array export const updateFilters = async (formData: FormData) => { const categoryFilters = formData.getAll("category"); const locationFilters = formData.getAll("location"); const spolFilters = formData.getAll("spol"); const starostFilters = formData.getAll("starost"); const allFilters = [ ...categoryFilters, ...locationFilters, ...spolFilters, ...starostFilters, ]; console.log("all filters", allFilters); if (allFilters.length > 0) { const params = new URLSearchParams([ ["category", categoryFilters.join(",")], ["location", locationFilters.join(",")], ["spol", spolFilters.join(",")], ["starost", starostFilters.join(",")], ]); console.log("params", params); redirect(/adoptPet?${params.toString()}); } else { redirect("/adoptPet"); } }; Is this looks well?
James4u
James4u6mo ago
sorry @tom_bombadil can you format your code?
export const updateFilters = async (formData: FormData) => {
const categoryFilters = formData.getAll("category");
const locationFilters = formData.getAll("location");
const spolFilters = formData.getAll("spol");
const starostFilters = formData.getAll("starost");

const allFilters = [
...categoryFilters,
...locationFilters,
...spolFilters,
...starostFilters,
];

console.log("all filters", allFilters);

if (allFilters.length > 0) {
const params = new URLSearchParams([
["category", categoryFilters.join(",")],
["location", locationFilters.join(",")],
["spol", spolFilters.join(",")],
["starost", starostFilters.join(",")],
]);
console.log("params", params);
redirect(/adoptPet?${params.toString()});
} else {
redirect("/adoptPet");
}
};
export const updateFilters = async (formData: FormData) => {
const categoryFilters = formData.getAll("category");
const locationFilters = formData.getAll("location");
const spolFilters = formData.getAll("spol");
const starostFilters = formData.getAll("starost");

const allFilters = [
...categoryFilters,
...locationFilters,
...spolFilters,
...starostFilters,
];

console.log("all filters", allFilters);

if (allFilters.length > 0) {
const params = new URLSearchParams([
["category", categoryFilters.join(",")],
["location", locationFilters.join(",")],
["spol", spolFilters.join(",")],
["starost", starostFilters.join(",")],
]);
console.log("params", params);
redirect(/adoptPet?${params.toString()});
} else {
redirect("/adoptPet");
}
};
tom_bombadil
tom_bombadilOP6mo ago
yes I do there is also some extra code related to onSubmit in the FilterMenu, ignore it for now because I'm calling the 'actions' function 'updateFilters' on onSubmit right now
James4u
James4u6mo ago
you mean, you use server actions?
tom_bombadil
tom_bombadilOP6mo ago
yes, that 'updateFilters' function is on the server side should I use classic handleSubmit in the Filter Menu? or this is fine?
James4u
James4u6mo ago
server actions are for data mutation indeed - you are just redirecting right? you can do that in the client side
tom_bombadil
tom_bombadilOP6mo ago
this is the log of that updateFilters function: all filters [ 'Macka', 'Tuzla', 'Zensko', 'Odraslo' ] params URLSearchParams { 'category' => 'Macka', 'location' => 'Tuzla', 'spol' => 'Zensko', 'starost' => 'Odraslo' } middleware is running on route: /adoptPet GET /adoptPet?category=Macka&location=Tuzla&spol=Zensko&starost=Odraslo 200 in 29ms in 'allFilters' I place what the user has chosen as a filter and then fill in the pathname as you can see below
James4u
James4u6mo ago
@tom_bombadil sorry but you are not answering my questions - difficult to keep conversation in efficient way
tom_bombadil
tom_bombadilOP6mo ago
what questions?
James4u
James4u6mo ago
checked this?
tom_bombadil
tom_bombadilOP6mo ago
let's go step by step okay, understand, you suggest to do this collecting data from the user directly on the FilterMenu component? no need for server actions?
James4u
James4u6mo ago
follow these @tom_bombadil - use https://nuqs.47ng.com/ this library and replce all of your states for filtering - when they click on button (or form submit), just router.refresh() - use those query params to filter out your data with prisma
nuqs | Type-safe search params state management for Next.js
Type-safe search params state management for Next.js. Like React.useState, but stored in the URL query string.
James4u
James4u6mo ago
yeah, server actions are for data mutation - you are only doing redirection in your current codebase which can be also done in the client component follow above steps - step by step and let me know if you have any trouble
tom_bombadil
tom_bombadilOP6mo ago
okay I will go with this and let you know if I get stuck @James4u I replaced all states in the FilterMenu component with useQueryState : const [category, setCategory] = useQueryState("category"); const [location, setLocation] = useQueryState("location"); const [gender, setGender] = useQueryState("gender"); const [age, setAge] = useQueryState("age"); what did you mean by 'submit' to do just router.refresh() ? you mean here to call router.refresh() : const handleSubmit = (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); setCategory(category); setLocation(location); setGender(gender); setAge(age); console.log(category, location, spol, starost);
James4u
James4u6mo ago
haha @tom_bombadil please format your code when you post here from next time and hmm why do you set states in handleSubmit?
setCategory(category);
setLocation(location);
setGender(gender);
setAge(age);
setCategory(category);
setLocation(location);
setGender(gender);
setAge(age);
those should be used in inputs
tom_bombadil
tom_bombadilOP6mo ago
just to see if i am getting value from jsx
James4u
James4u6mo ago
No description
tom_bombadil
tom_bombadilOP6mo ago
<div className="flex flex-col flex-1"> <label htmlFor="category" className="mb-1 text-black text-[12px]"> Kategorija </label> <select id="category" name="category" value={category || ""} onChange={(e) => setCategory(e.target.value)} className="p-2 border rounded-[24px] bg-white text-[#A4A4A4] text-[12px]" > <option value="Pas">Pas</option> <option value="Macka">Mačka</option> <option value="Ostalo">Ostale životinje</option> </select> </div>
James4u
James4u6mo ago
oh you are doing that! if you see the values (what you type) in the input, it's working correct
tom_bombadil
tom_bombadilOP6mo ago
yes but what with router.refresh()
James4u
James4u6mo ago
don't need to set states in the submit handler
tom_bombadil
tom_bombadilOP6mo ago
I will remove it what is next step bro 😄
James4u
James4u6mo ago
and where do you fetch data? using prisma
tom_bombadil
tom_bombadilOP6mo ago
on the parent component
James4u
James4u6mo ago
show me the code
tom_bombadil
tom_bombadilOP6mo ago
sure
James4u
James4u6mo ago
please format your code 🙏
No description
tom_bombadil
tom_bombadilOP6mo ago
i thought i always format by tagging everything and taking the '<>' sign, is that ok?
James4u
James4u6mo ago
nonpe
tom_bombadil
tom_bombadilOP6mo ago
with single ``
James4u
James4u6mo ago
3 ` and file extension
tom_bombadil
tom_bombadilOP6mo ago
oh didn't know that sry mate will do now
type Post = {
id: string;
post_id: string;
imageUrls: string[];
location: string;
username: string;
category: string;
petName: string;
vakcinisan: string;
cipovan: string;
pasos: string;
spol: string;
starost: string;
phoneNumber: string;
description: string;
createdAt: Date;
updatedAt: Date;
};

export default function AdoptPet() {
const router = useRouter();
const searchParams = useSearchParams();
const [posts, setPosts] = useState<Post[]>([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(12);
const [total, setTotal] = useState(0);
const [isPending, startTransition] = useTransition(); // loading state
const [isLoading, setIsLoading] = useState(true); // Add loading state

const fetchData = (page: number) => {
startTransition(async () => {
const result = await getAdoptPost({
query: { page },
});

setPosts(result.post);
setPage(result.page);
setPageSize(result.pageSize);
setTotal(result.total);
setIsLoading(false); // Set loading to false after data is fetched

});
};

useEffect(() => {
const currentPage = parseInt(searchParams.get("page") || "1", 12);

fetchData(currentPage);
}, [searchParams]);

const handlePagination = (newPage: number) => {
setIsLoading(true); // Set loading to true when pagination changes
const params = new URLSearchParams(searchParams);
params.set("page", newPage.toString());
router.push(`/adoptPet?${params.toString()}`);
};

const handleNextPage = () => {
if (page < Math.ceil(total / pageSize)) {
handlePagination(page + 1);
}
};

const handlePreviousPage = () => {
if (page > 1) {
handlePagination(page - 1);
}
};
...
{posts.map((animal) => (
<div key={animal.id} className="p-4 bg-white rounded-xl shadow-md">
...
type Post = {
id: string;
post_id: string;
imageUrls: string[];
location: string;
username: string;
category: string;
petName: string;
vakcinisan: string;
cipovan: string;
pasos: string;
spol: string;
starost: string;
phoneNumber: string;
description: string;
createdAt: Date;
updatedAt: Date;
};

export default function AdoptPet() {
const router = useRouter();
const searchParams = useSearchParams();
const [posts, setPosts] = useState<Post[]>([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(12);
const [total, setTotal] = useState(0);
const [isPending, startTransition] = useTransition(); // loading state
const [isLoading, setIsLoading] = useState(true); // Add loading state

const fetchData = (page: number) => {
startTransition(async () => {
const result = await getAdoptPost({
query: { page },
});

setPosts(result.post);
setPage(result.page);
setPageSize(result.pageSize);
setTotal(result.total);
setIsLoading(false); // Set loading to false after data is fetched

});
};

useEffect(() => {
const currentPage = parseInt(searchParams.get("page") || "1", 12);

fetchData(currentPage);
}, [searchParams]);

const handlePagination = (newPage: number) => {
setIsLoading(true); // Set loading to true when pagination changes
const params = new URLSearchParams(searchParams);
params.set("page", newPage.toString());
router.push(`/adoptPet?${params.toString()}`);
};

const handleNextPage = () => {
if (page < Math.ceil(total / pageSize)) {
handlePagination(page + 1);
}
};

const handlePreviousPage = () => {
if (page > 1) {
handlePagination(page - 1);
}
};
...
{posts.map((animal) => (
<div key={animal.id} className="p-4 bg-white rounded-xl shadow-md">
...
this is the page where I render all animals and where want to filter that 'posts' array
export async function getAdoptPost(context: any) {
const page = parseInt((context.query.page as string) || "1", 12);
const pageSize = 12;

const total = await db.adoptAnimal.count({});

const post = await db.adoptAnimal.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: {
createdAt: "desc",
},
});

return {
post,
page,
pageSize,
total,
};
}
export async function getAdoptPost(context: any) {
const page = parseInt((context.query.page as string) || "1", 12);
const pageSize = 12;

const total = await db.adoptAnimal.count({});

const post = await db.adoptAnimal.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: {
createdAt: "desc",
},
});

return {
post,
page,
pageSize,
total,
};
}
and this is server function which using prisma @James4u should provide you some more code?
James4u
James4u6mo ago
oh sorry couldn't see the noti - give me some time
tom_bombadil
tom_bombadilOP6mo ago
sure tnx mate
James4u
James4u6mo ago
hmm you are doing client side data fetching not a deal breaker but you can use server side data fetching if you are using app router
tom_bombadil
tom_bombadilOP6mo ago
if we can leave it on the client side, then let it stay if it will not create a problem related to filtering
James4u
James4u6mo ago
and also
const [isPending, startTransition] = useTransition();
const [isPending, startTransition] = useTransition();
you have isPending - why you need another state for the loading state? setIsLoading(false); // Set loading to false after data is fetched btw why don't you pass filter params to the api so that you can use them when you get data from prisma? or was it your original question?
tom_bombadil
tom_bombadilOP6mo ago
yes, that's actually my question, how will I now apply my filters that I picked up in the FilterMenu, on this AdoptPet page where all the animals are rendered. It is not the clearest to me with the prisma of how to bind, if you can help me with that?
James4u
James4u6mo ago
so before you hit the api, here
const result = await getAdoptPost({
query: { page },
});
const result = await getAdoptPost({
query: { page },
});
you can get query params from the url, right?
tom_bombadil
tom_bombadilOP6mo ago
sorry, what do you want to say? http://localhost:3000/adoptPet?category=Dog&location=Sarajevo&spol=Man&starost=Adult if you ask for this, I'm getting params in the pathame while picking up filter values from FilterMenu there is also pagination, which is done in the same function on prisma, so don't get confused 🙂 and can you give a little more explanation of what you mean by 'hit the api'? @James4u lemme know when you're available to continue discuss about this?
James4u
James4u6mo ago
const result = await getAdoptPost({
query: { page },
});
const result = await getAdoptPost({
query: { page },
});
you need to pass more fitler values when you hit this endpoint, right?
tom_bombadil
tom_bombadilOP6mo ago
it is clear to me that I have to send the filtered values, how would the function in prisma look then, something like this?
export async function getAdoptPost(
context: any,
filters: {
category?: string;
location?: string;
spol?: string;
starost?: string;
} = {}
) {
const page = parseInt((context.query.page as string) || "1", 12);
const pageSize = 12;

const total = await db.adoptAnimal.count({
where: filters,
});

const post = await db.adoptAnimal.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
where: filters,
orderBy: {
createdAt: "desc",
},
});

return {
post,
page,
pageSize,
total,
};
}
export async function getAdoptPost(
context: any,
filters: {
category?: string;
location?: string;
spol?: string;
starost?: string;
} = {}
) {
const page = parseInt((context.query.page as string) || "1", 12);
const pageSize = 12;

const total = await db.adoptAnimal.count({
where: filters,
});

const post = await db.adoptAnimal.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
where: filters,
orderBy: {
createdAt: "desc",
},
});

return {
post,
page,
pageSize,
total,
};
}
and then on the client side:
const [filters, setFilters] = useState<AnimalSearchParams>({});

const fetchData = (page: number, filters: AnimalSearchParams) => {
startTransition(async () => {
const result = await getAdoptPost({ query: { page } }, filters);

setPosts(result.post);
setPage(result.page);
setPageSize(result.pageSize);
setTotal(result.total);
setIsLoading(false); // Set loading to false after data is fetched
});
};

useEffect(() => {
const currentPage = parseInt(searchParams.get("page") || "1", 12);

fetchData(currentPage, filters);
}, [searchParams, filters]);
const [filters, setFilters] = useState<AnimalSearchParams>({});

const fetchData = (page: number, filters: AnimalSearchParams) => {
startTransition(async () => {
const result = await getAdoptPost({ query: { page } }, filters);

setPosts(result.post);
setPage(result.page);
setPageSize(result.pageSize);
setTotal(result.total);
setIsLoading(false); // Set loading to false after data is fetched
});
};

useEffect(() => {
const currentPage = parseInt(searchParams.get("page") || "1", 12);

fetchData(currentPage, filters);
}, [searchParams, filters]);
@James4u
James4u
James4u6mo ago
Yeah, they seems to be correct but again, you may not need isLoading state
tom_bombadil
tom_bombadilOP6mo ago
I will remove it, but that doesn't affect the filtering problem
Want results from more Discord servers?
Add your server