3

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>
  );
}
squish
  • 846
  • 12
  • 24
  • That component is doing too many things. I'd recommend breaking it into smaller components to contain the various states and avoid rerendering everything whenever one of the states changes. Also, `shallow: true` does not prevent rerenders, it simply prevents data fetching methods (`getServerSideProps`, `getStaticProps`) from being called. – juliomalves Dec 16 '21 at 23:57
  • Definitely, will need to break it up so its more readable. Ah I see, so If i wanted the user to change search parameters, I need to somehow search, update the URL but not force a page reload. Thank you for the reply! @juliomalves – squish Dec 17 '21 at 10:58

0 Answers0