I am creating a search page. You can have a search query, e.g. "Nike" and then you can click different categories or conditions.
My file structure in pages
is just:
search
> index.tsx
When a user changes a category or condition, the search works correctly and finds the correct results. The search query is also correct. Here is a sample query:
/search?searchQuery=nike&category=clothes
However, the page reloads, which causes UI elements to move around, expanding menu's close etc.
I need the page to research without reloading, I read that using shallow: true
should avoid this behaviour but it isn't working at the moment. I am only updating the query params.
Here is my (search
) index.tsx:
export default function Search() {
const [listings, setListings] = useState<null | Array<ListingObject>>(null);
const [noResultsListings, setNoResultsListings] = useState<
Array<ListingObject>
>([]);
const [loading, setLoading] = useState("idle");
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const [sortBy, setSortBy] = useState("listings");
const [currentPage, setCurrentPage] = useState(0);
const [paginationData, setPaginationData] = useState<PaginationData>({
totalHits: 0,
totalPages: 0,
});
const router = useRouter();
const { query, isReady } = useRouter();
let searchClient;
if (process.env.NODE_ENV === "development") {
searchClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_TEST_APP!,
process.env.NEXT_PUBLIC_ALGOLIA_TEST_API!
);
}
useEffect(() => {
if (query && sortBy && loading === "idle" && isReady) {
console.log("Search use effect ran");
search(query, 0);
}
}, [isReady, query]);
//============================================
//When a search filter checkbox is checked/unchecked, this is called
//============================================
function handleCheck(checked: Boolean, id: string, option) {
let tempQuery = query;
if (checked) {
if (tempQuery[id]) {
tempQuery[id] += `,${option.value}`;
} else {
tempQuery[id] = `${option.value}`;
}
}
if (!checked) {
if (tempQuery[id] && tempQuery[id].toString().split(",").length > 1) {
let param = tempQuery[id].toString();
let array = param.split(",");
tempQuery[id] = array.filter((param) => param !== option.value);
} else {
delete query[id];
}
}
router.push(
{
pathname: `/search`,
query: tempQuery,
},
undefined,
{ shallow: true }
);
}
//============================================
//Where the "search" actually happens.
//============================================
async function search(query, page: number) {
if (loading === "idle") {
setListings(null);
setLoading("loading");
let listingIndex = searchClient.initIndex(sortBy);
const readyQuery = await handleSearchQuery(query);
const response = await listingIndex.search(readyQuery.searchQuery, {
numericFilters: readyQuery.filters.numericFilters,
facetFilters: readyQuery.filters.facetFilters,
page: page,
});
if (response) {
const hits = response.hits;
setPaginationData({
totalPages: response.nbPages,
totalHits: response.nbHits,
});
console.log(hits);
setListings(hits);
setLoading("idle");
}
}
}
//=========================================================
//When a user changes page, uses npm package react-paginate
//=========================================================
const handlePageClick = (data) => {
let selected = data.selected;
setCurrentPage(data.selected);
search(query, selected);
window.scrollTo(0, 0);
};
return (
<div className="flex flex-col items-center bg-white min-h-screen">
<Navbar page={"Search"} />
<div className="container">
{/* Mobile filter dialog */}
<Transition.Root show={mobileFiltersOpen} as={Fragment}>
<Dialog
as="div"
className="fixed inset-0 flex z-40 lg:hidden"
onClose={setMobileFiltersOpen}
>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="ml-auto relative max-w-xs w-full h-full bg-white shadow-xl py-4 pb-12 flex flex-col overflow-y-auto">
<div className="px-4 flex items-center justify-between">
<h2 className="text-lg font-medium text-gray-900">Filters</h2>
<button
type="button"
className="-mr-2 w-10 h-10 bg-white p-2 rounded-md flex items-center justify-center text-gray-400"
onClick={() => setMobileFiltersOpen(false)}
>
<span className="sr-only">Close menu</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
{/* Filters */}
<form className="mt-4 border-t border-gray-200">
{filters.map((section) => (
<Disclosure
as="div"
key={section.id}
className="border-t border-gray-200 px-4 py-6"
>
{({ open }) => (
<>
<h3 className="-my-3 flow-root">
<Disclosure.Button className="px-2 py-3 bg-white w-full flex items-center justify-between text-gray-400 hover:text-gray-500">
<span className="font-medium text-gray-900">
{section.name}
</span>
<span className="ml-6 flex items-center">
{open ? (
<MinusSmIcon
className="h-5 w-5"
aria-hidden="true"
/>
) : (
<PlusSmIcon
className="h-5 w-5"
aria-hidden="true"
/>
)}
</span>
</Disclosure.Button>
</h3>
<Disclosure.Panel className="pt-6">
<div className="space-y-6">
{section.options.map((option, optionIdx) => (
<div
key={option.value}
className="flex items-center"
>
<input
checked={router?.query[
section.id
]?.includes(option.value)}
id={`filter-mobile-${section.id}-${optionIdx}`}
name={`${section.id}[]`}
defaultValue={option.value}
type="checkbox"
className="h-4 w-4 border-gray-300 rounded text-indigo-600 focus:ring-indigo-500"
/>
<label
htmlFor={`filter-mobile-${section.id}-${optionIdx}`}
className="ml-3 min-w-0 flex-1 text-gray-500"
>
{option.label}
</label>
</div>
))}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
</form>
</div>
</Transition.Child>
</Dialog>
</Transition.Root>
<main className="w-full mx-auto px-4 md:px-0">
{listings?.length > 0 && (
<div className="flex flex-col relative z-30 md:items-center justify-between pb-6 md:flex-row">
<div className="flex items-center mt-8">
<Menu as="div" className="relative inline-block text-left">
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-gray-700 hover:text-gray-900">
{" "}
{sortOptions.find((item) => item.value === sortBy).label}
<ChevronDownIcon
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute left-0 md:right-0 mt-2 w-48 p-0 shadow-2xl bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-30">
<div>
{sortOptions.map((option) => (
<Menu.Item key={option.value}>
{({ active }) => (
<button
className={classNames(
sortBy === option.value
? "font-medium text-gray-900"
: "text-gray-500",
active ? "bg-gray-100" : "",
"block px-4 py-2 text-sm w-full text-left"
)}
onClick={() => {
setSortBy(option.value);
}}
>
{option.label}
</button>
)}
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
<button
type="button"
className="p-2 -m-2 ml-4 sm:ml-6 text-gray-400 hover:text-gray-500 lg:hidden"
onClick={() => setMobileFiltersOpen(true)}
>
<span className="sr-only">Filters</span>
<FilterIcon className="w-5 h-5" aria-hidden="true" />
</button>
</div>
</div>
)}
<section className="flex w-full">
<div className="hidden md:grid grid-cols-1 lg:grid-cols-1 min-h-screen w-1/5 lg:pr-4">
{/* Filters */}
<form className="hidden h-full overflow-y-auto md:inline-block">
{listings?.length > 0 &&
filters.map((section) => (
<Disclosure
as="div"
key={section.id}
className="border-gray-200 py-6"
>
{({ open }) => (
<>
<h3 className="-my-3 flow-root">
<Disclosure.Button className="py-3 bg-white w-full flex items-center justify-between text-sm text-gray-400 hover:text-gray-500">
<span className="font-medium text-gray-900">
{section.name}
</span>
<span className="ml-6 flex items-center">
{open ? (
<MinusSmIcon
className="h-5 w-5"
aria-hidden="true"
/>
) : (
<PlusSmIcon
className="h-5 w-5"
aria-hidden="true"
/>
)}
</span>
</Disclosure.Button>
</h3>
<Disclosure.Panel className="pt-6">
<div className="space-y-4 px-1">
{section.options.map((option, optionIdx) => (
<div
key={option.value}
className="flex items-center"
>
<input
checked={
query[section.id] &&
query[section.id]
?.toString()
.split(",")
.includes(option.value)
? true
: false
}
id={`filter-${section.id}-${optionIdx}`}
name={`${section.id}[]`}
type="checkbox"
className="checkbox"
onChange={(e) => {
handleCheck(
e.target.checked,
section.id,
option
);
}}
/>
<label
htmlFor={`filter-${section.id}-${optionIdx}`}
className="ml-3 text-sm text-gray-600"
>
{option.label}
</label>
</div>
))}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
</form>
</div>
{/* Product grid */}
<ul
role="list"
className="h-full w-full grid grid-cols-1 py-4 gap-8 md:grid-cols-4 md:w-4/5 md:p-4 xl:gap-x-8"
>
{!listings && <SearchGridSkeleton />}
{listings?.length > 0 &&
listings.map((listing) => (
<ListingCard
key={listing.docID}
listing={listing}
type={"normal"}
/>
))}
{listings?.length === 0 && (
<div className="w-full h-full flex flex-col items-center justify-center col-span-4 relative py-8">
<h2 className="text-lg font-semibold text-center">
Oops, we found no results for "
<span className="underline text-blue-600">
{query.searchQuery}
</span>
"
</h2>
<div className="mt-16 flex flex-col justify-center items-center w-full md:w-3/4">
<h3 className="text-base font-semibold">
You may also like:
</h3>
<div className="w-full grid gap-8 mt-8 border-t pt-8 grid-cols-1 md:grid-cols-3">
{noResultsListings?.map((listing: ListingObject) => (
<ListingCard
className="col-span-1"
key={listing.docID}
type="normal"
listing={listing}
/>
))}
</div>
</div>
</div>
)}
</ul>
</section>
{listings?.length > 0 && (
<ReactPaginate
previousLabel={
<span className="flex items-center">
<ArrowNarrowLeftIcon
className="mr-3 h-5 w-5 text-gray-400"
aria-hidden="true"
/>{" "}
Previous
</span>
}
nextLabel={
<span className="flex items-center">
Next{" "}
<ArrowNarrowRightIcon
className="ml-3 h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
}
breakLabel={"..."}
breakClassName={"break-me"}
pageCount={paginationData.totalPages}
marginPagesDisplayed={2}
pageRangeDisplayed={10}
onPageChange={handlePageClick}
disabledClassName={"opacity-20"}
pageClassName={
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 border-t-2 pt-4 px-4 inline-flex items-center text-sm font-medium"
}
containerClassName={
"border-t border-gray-200 px-4 flex items-center justify-center sm:px-0 pb-8"
}
activeClassName={"border-indigo-500 text-indigo-600"}
nextClassName={
"border-t-2 border-transparent pt-4 pr-1 inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 ml-auto"
}
previousClassName={
"border-t-2 border-transparent pt-4 pr-1 inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 mr-auto"
}
initialPage={0}
/>
)}
</main>
</div>
<Footer />
</div>
);
}