iTranslated by AI
Next.js 3-Minute Recipe: Implementing View Filtering and Toggle Displays
Introduction
I was taught a way to quickly implement filtering for components displayed on the View side by my mentor, so I'd like to leave it here as a "3-minute cooking" style memo for future reference.
Conclusion
You will be able to search like this.

Below is the Vercel link.
Instead of fetching and displaying every time, it is configured to show the data from the cache if it exists.
I got a bit lazy, so for Vercel, I implemented it within the same App as the previous article and just changed the reference page 🙇 (cutting corners).
Previous article is here (check it out if you like):
Ingredients
| Name | version | Remarks |
|---|---|---|
| Next.js | 14.2.5 | React is also fine |
| @tanstack/react-query | 5.51.11 | Required |
| use-debounce | 10.0.1 | Not necessary if you implement it yourself |
| tailwind css | 3.4.1 | Only needed for styling, so it's optional |
Preparation
Prepare components, etc.
TanstackProvider.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { type ReactNode } from 'react';
type TanstackProviderProps = {
children: ReactNode;
};
const TanstackProvider = ({ children }: TanstackProviderProps) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
export default TanstackProvider;
CuisineList.tsx
import Image from 'next/image';
import { Cuisines } from '@/type/cuisine';
type CuisineListProps = {
cuisines: Cuisines[];
};
export const CuisineList = ({ cuisines }: CuisineListProps) => {
return (
<div className="flex flex-row flex-wrap gap-4">
{cuisines.map((cuisine) => (
<div
key={cuisine.id}
className="flex w-[250px] flex-col gap-4 rounded-md bg-gray-700 p-4"
>
<div className="flex flex-row justify-between">
<div>
<h2 className="text-xl font-bold">{cuisine.name}</h2>
<p className="opacity-50">{cuisine.category}</p>
</div>
<p>${cuisine.price}</p>
</div>
<Image
src={cuisine.image}
alt={cuisine.name}
width={150}
height={150}
className="rounded-md"
/>
</div>
))}
</div>
);
};
Cuisines.ts
export type Cuisines = {
id: number;
name: string;
category?: 'Japanese' | 'Italian' | 'French';
price: number;
image: string;
};
CuisinesPage
'use client';
import { useState } from 'react';
import { CuisineList } from '@/components/cuisines/CuisinesList';
import { CuisinesListFilter } from '@/components/cuisines/CuisinesListFilter';
const CuisinesPage = () => {
return (
<div className="flex flex-col gap-2">
<div>
<h1 className="text-4xl font-bold">Cuisines</h1>
</div>
<CuisinesListFilter />
<div>
<CuisineList />
</div>
</div>
);
};
export default CuisinesPage;
Prepare a mock endpoint
/api/cuisines.ts
import { CUISINES } from './data/cuisinesData';
export type CuisineFilters = {
category?: 'Japanese' | 'Italian' | 'French';
maxPrice?: number;
search?: string;
};
export type Cuisine = {
id: number;
name: string;
category?: 'Japanese' | 'Italian' | 'French';
price: number;
image: string;
};
export const fetchCuisines = async (
options?: CuisineFilters
): Promise<Cuisine[]> => {
await new Promise((resolve) => setTimeout(resolve, 1000));
let filteredCuisines = CUISINES;
if (options?.category) {
filteredCuisines = filteredCuisines.filter((cuisine) => {
return cuisine.category === options.category;
});
}
if (options?.maxPrice) {
filteredCuisines = filteredCuisines.filter((cuisine) => {
return cuisine.price <= (options.maxPrice as number);
});
}
if (options?.search) {
filteredCuisines = filteredCuisines.filter((cuisine) => {
return cuisine.name.toLowerCase().includes(options.search!.toLowerCase());
});
}
return filteredCuisines;
};
/api/data/cusinesData.ts
import { Cuisine } from '../cuisines';
export const CUISINES: Cuisine[] = [
{
id: 1,
name: 'Food 1',
category: 'Japanese',
price: 10,
image: 'https://via.placeholder.com/150',
},
{
id: 2,
name: 'Food 2',
category: 'Italian',
price: 20,
image: 'https://via.placeholder.com/150',
},
{
id: 3,
name: 'Food 3',
category: 'French',
price: 30,
image: 'https://via.placeholder.com/150',
},
{
id: 4,
name: 'Food 4',
category: 'Japanese',
price: 40,
image: 'https://via.placeholder.com/150',
},
{
id: 5,
name: 'Food 5',
category: 'Italian',
price: 50,
image: 'https://via.placeholder.com/150',
},
{
id: 6,
name: 'Food 6',
category: 'French',
price: 60,
image: 'https://via.placeholder.com/150',
},
{
id: 7,
name: 'Food 7',
category: 'Japanese',
price: 70,
image: 'https://via.placeholder.com/150',
},
{
id: 8,
name: 'Food 8',
category: 'Italian',
price: 80,
image: 'https://via.placeholder.com/150',
},
{
id: 9,
name: 'Food 9',
category: 'French',
price: 90,
image: 'https://via.placeholder.com/150',
},
{
id: 10,
name: 'Food 10',
category: 'Japanese',
price: 100,
image: 'https://via.placeholder.com/150',
},
];
Start Cooking
Step 1
Set the values used for searching in the Page using useState.
const [search, setSearch] = useState<CuisineFilters['search']>();
const [category, setCategory] = useState<CuisineFilters['category']>();
const [maxPrice, setMaxPrice] = useState<CuisineFilters['maxPrice']>();
Step 2
Use TanStack Query within the Page to enable data fetching based on the corresponding conditions.
const { data, isFetching } = useQuery({
queryKey: ['cuisines', { category, maxPrice, search }],
queryFn: () => fetchCuisines({ category, maxPrice, search }),
});
Step 3
Set a callback event in the CuisinesListFilter props so that when it is called in the child component, the values on the parent side are updated.
<CuisinesListFilter
onChange={(filters) => {
setCategory(filters.category);
setMaxPrice(filters.maxPrice);
setSearch(filters.search);
}}
/>
Step 4
Enable the child component to detect state changes using useEffect.
* For text search, implement debounce processing to set the timing.
import { useEffect, useState } from 'react';
import { useDebounce } from 'use-debounce';
import { type CuisineFilters } from '@/app/api/cuisines';
type CuisinesListFilterProps = {
onChange: (filters: CuisineFilters) => void;
};
export const CuisinesListFilter = ({ onChange }: CuisinesListFilterProps) => {
const [search, setSearch] = useState<CuisineFilters['search']>();
const [category, setCategory] = useState<CuisineFilters['category'] | 'all'>(
'all'
);
const [maxPrice, setMaxPrice] = useState<CuisineFilters['maxPrice'] | 'all'>(
'all'
);
const [debouncedSearch] = useDebounce(search, 500);
useEffect(() => {
const filters: CuisineFilters = {
category: category === 'all' ? undefined : category,
maxPrice: maxPrice === 'all' ? undefined : maxPrice,
search: debouncedSearch,
};
onChange(filters);
}, [category, debouncedSearch, maxPrice, onChange]);
return (
<div className="flex flex-row gap-2">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search Cuisines"
/>
<select
value={category}
onChange={(e) =>
setCategory(e.target.value as CuisineFilters['category'] | 'all')
}
>
<option value="all">all</option>
<option value="Japanese">Japanese</option>
<option value="Italian">Italian</option>
<option value="French">French</option>
</select>
<select
value={maxPrice}
onChange={(e) =>
setMaxPrice(
e.target.value !== 'all' ? parseInt(e.target.value) : 'all'
)
}
>
<option value="all">all</option>
<option value="10">$10</option>
<option value="50">$50</option>
<option value="100">$100</option>
</select>
</div>
);
};
By following the steps above, you can trigger TanStack Query processing in the parent based on change events from the child, making it easy to implement search functionality.
Summary
Just like with "3-minute cooking," you might think, "If you include the preparation, it's not 3 minutes at all!" However, since it allows for easy view switching using filtering, I plan to use this as a foundation for quickly implementing search functionality and similar processes.
The repository is here (hope it helps):
Side Note
When I looked up words for food, I found that terms for specific styles of cooking (like Japanese or French cuisine) are a bit formal, but the word cuisine came up, so I'm using that.
I wondered what today's recipe (at the time of writing) was and checked; it was Acqua Pazza lol.
Even the original show's recipes were mostly like "There's no way you can do this in 3 minutes...", so I feel relieved lol.
Discussion