Next.js Discord

Discord Forum

Loading state for Pagination

Answered
Arinji posted this in #help-forum
Open in Discord
I was going through this https://nextjs.org/learn/dashboard-app/adding-search-and-pagination

and wanted to know how would i go about a loading state?
Currently i have another searchParam called search which i set to true when i want to search, and i have my loader look for this search param.

Issue is when im done with the data fetching, if i just do redirect() with the old url but removing the search param, the data isnt rendered anymore which i think is because the page changed.

So i just want to know how do i have a search button which when clicked show a loading ui and then after the data is searched, the loader is turned off and it wont search again until i click the search button again.
Answered by Ray
const [isPending, startTransition] = useTransition()
const router = useRouter()

const handleClick = () => {
  startTransition(() => {
    router.push("")
  })
}
View full answer

93 Replies

hi ray
Answer
wait so put this in the loading component?
export function SearchInput({ completed }: { completed: boolean }) {
  const [loading, setLoading] = useState(false);
  const router = useRouter();
  const searchParams = useSearchParams();
  const pathName = usePathname();

  useEffect(() => {
    console.log(completed === loading);
    if (completed === loading) {
      setLoading(!completed);
    }
  }, [completed, loading]);

  useEffect(() => {
    console.log(loading);
  }, [loading]);

  const handleSearch = () => {
    const params = new URLSearchParams(searchParams);
    const query = searchParams.get("query")?.toString();
    const limit = searchParams.get("limit")?.toString();

    if (query && limit) params.set("search", "true");
    else {
      params.delete("search");
      setLoading(false);
    }

    router.replace(`${pathName}?${params.toString()}`);
  };

  useEffect(() => {
    if (completed) return;
    if (searchParams.get("search")?.toString()) {
      setLoading(true);
    } else {
      setLoading(false);
    }
  }, [searchParams]);
  return (
    <div
      className={cn(
        "w-full h-full flex flex-row items-center justify-start gap-5",
        {
          "pointer-events-none opacity-50":
            !searchParams.get("query")?.toString() ||
            !searchParams.get("limit")?.toString(),
        }
      )}
    >
      <button
        onClick={() => {
          handleSearch();
          setLoading(true);
        }}
        className="w-[50px] bg-shades-lightgrey p-2 gap-1 h-full flex flex-col items-center justify-center "
      >
        {loading ? (
          <Loader2 className="w-[20px] h-[20px] text-shades-cyan animate-spin " />
        ) : (
          <Search className="w-[20px] h-[20px] text-shades-cyan " />
        )}
      </button>
    </div>
  );
}
thats my current loading button, (as you can see its completely shit)
the completed param is a flag i made in my page which i set to false at the top and then true at the end
@Arinji jsx export function SearchInput({ completed }: { completed: boolean }) { const [loading, setLoading] = useState(false); const router = useRouter(); const searchParams = useSearchParams(); const pathName = usePathname(); useEffect(() => { console.log(completed === loading); if (completed === loading) { setLoading(!completed); } }, [completed, loading]); useEffect(() => { console.log(loading); }, [loading]); const handleSearch = () => { const params = new URLSearchParams(searchParams); const query = searchParams.get("query")?.toString(); const limit = searchParams.get("limit")?.toString(); if (query && limit) params.set("search", "true"); else { params.delete("search"); setLoading(false); } router.replace(`${pathName}?${params.toString()}`); }; useEffect(() => { if (completed) return; if (searchParams.get("search")?.toString()) { setLoading(true); } else { setLoading(false); } }, [searchParams]); return ( <div className={cn( "w-full h-full flex flex-row items-center justify-start gap-5", { "pointer-events-none opacity-50": !searchParams.get("query")?.toString() || !searchParams.get("limit")?.toString(), } )} > <button onClick={() => { handleSearch(); setLoading(true); }} className="w-[50px] bg-shades-lightgrey p-2 gap-1 h-full flex flex-col items-center justify-center " > {loading ? ( <Loader2 className="w-[20px] h-[20px] text-shades-cyan animate-spin " /> ) : ( <Search className="w-[20px] h-[20px] text-shades-cyan " /> )} </button> </div> ); }
replace the loading state with
const [isPending, startTransition] = useTransition()
then
startTransition(() => {
   router.replace(`${pathName}?${params.toString()}`);
})
isPending will be the loading state
Ohh ok one sec
lemme try
and remove all of the useEffect
yea
export function SearchInput({  }: {  }) {
  const [isPending, startTransition] = useTransition();
  const router = useRouter();
  const searchParams = useSearchParams();
  const pathName = usePathname();


  const handleSearch = () => {
    const params = new URLSearchParams(searchParams);
    const query = searchParams.get("query")?.toString();
    const limit = searchParams.get("limit")?.toString();

    if (query && limit) params.set("search", "true");
    else {
      params.delete("search");
      
    }

   startTransition(() => {
     router.replace(`${pathName}?${params.toString()}`);
   });
  };

  return (
    <div
      className={cn(
        "w-full h-full flex flex-row items-center justify-start gap-5",
        {
          "pointer-events-none opacity-50":
            !searchParams.get("query")?.toString() ||
            !searchParams.get("limit")?.toString(),
        }
      )}
    >
      <button
        onClick={() => {
          handleSearch();
          
        }}
        className="w-[50px] bg-shades-lightgrey p-2 gap-1 h-full flex flex-col items-center justify-center "
      >
        {isPending ? (
          <Loader2 className="w-[20px] h-[20px] text-shades-cyan animate-spin " />
        ) : (
          <Search className="w-[20px] h-[20px] text-shades-cyan " />
        )}
      </button>
    </div>
  );
}
@Ray and remove all of the useEffect
All good now?
yes
oki lemme test
@Ray yes
ok works perfectly, now the only other issue is the search param, i set it to true when you click on the Search button, but this causes any change to the url to also be searched
like how do i set it to false or remove it after the data has been fetched
or is there a better way to do this?
what is that for?
ok so once you select a limit, and enter a message
you click on the search icon
the page which calls all these components waits for the search button to be clicked, and then checks the url for errors, if it finds nothing wrong it fetches the data and renders it below
but since i dont ever set search to false, if i click on 30 or 5.. it re renders the data
@Arinji ok so once you select a limit, and enter a message
try
 if (query && limit) window.history.pushState(null, "", `?search=true`);;
    else {
      params.delete("search");
      
    }
uhh in the search component right?
search button
in side handleSearch
oki one sec
oh wait
waiting
you dont have to use the url, if there is a better way im happy to do that
i just dont know a better way to do this than the url
you got what im trying to do right?
where do you use the search value?
just at the page,
export default async function Page({
  searchParams,
}: {
  searchParams?: {
    query?: string;
    page?: string;
    limit?: string;
    search?: string;
  };
}) {
  const user = await GetUser();

  let notifications = "";
  let disableInput = false;
  let documents: TicketMessageSchemaType[] = [];
  if (searchParams) {
    const { query, limit, search, page } = searchParams;

    if (!query || !limit || !search) {
      disableInput = false;
    } else {
      let currentPage = 0;
      if (!page || isNaN(parseInt(page))) currentPage = 0;
      else currentPage = parseInt(page);
      disableInput = true;
      const searchResults = await SearchTicketMessage(
        query,
        parseInt(limit),
        currentPage
      );
      console.log(query, parseInt(limit), currentPage, searchResults);
      if (searchResults.success) {
        searchResults.document.forEach((message) => {
          const parsedMessage = TicketMessageSchema.safeParse(message);

          console.log(parsedMessage);

          if (parsedMessage.success) {
            if (parsedMessage.data.deleted.is_deleted && !user.isStaff) return;
            else documents.push(parsedMessage.data);
          }
        });
        console.log(documents);
      }
      if (documents.length === 0) notifications = "No results found.";
      else notifications = `Found  results.`;

      disableInput = false;
    }
  }
`
@Arinji ok so once you select a limit, and enter a message
you only want to show the result when they press the button?
@Arinji ye
could you show the code on the button
5,15,30
uh which button?
search button right?
export function SearchInput({}: {}) {
  const [isPending, startTransition] = useTransition();
  const router = useRouter();
  const searchParams = useSearchParams();
  const pathName = usePathname();

  const handleSearch = () => {
    const params = new URLSearchParams(searchParams);
    const query = searchParams.get("query")?.toString();
    const limit = searchParams.get("limit")?.toString();

    if (query && limit) params.set("search", "true");
    else {
      params.delete("search");
    }

    startTransition(() => {
      router.replace(`${pathName}?${params.toString()}`);
    });
  };

  return (
    <div
      className={cn(
        "w-full h-full flex flex-row items-center justify-start gap-5",
        {
          "pointer-events-none opacity-50":
            !searchParams.get("query")?.toString() ||
            !searchParams.get("limit")?.toString(),
        }
      )}
    >
      <button
        onClick={() => {
          handleSearch();
        }}
        className="w-[50px] bg-shades-lightgrey p-2 gap-1 h-full flex flex-col items-center justify-center "
      >
        {isPending ? (
          <Loader2 className="w-[20px] h-[20px] text-shades-cyan animate-spin " />
        ) : (
          <Search className="w-[20px] h-[20px] text-shades-cyan " />
        )}
      </button>
    </div>
  );
}
@Ray you only want to show the result when they press the button?
yes, so click the button once, then it shows the data and then dosent show again till you click the button again
oh limits, one sec
export function LimitInput({ user }: { user: DiscordUserSchemaType }) {
  const router = useRouter();
  const searchParams = useSearchParams();
  const pathName = usePathname();

  const handleSearch = (limit: string) => {
    const params = new URLSearchParams(searchParams);
    if (limit) {
      params.set("limit", limit);
    } else {
      params.delete("limit");
    }
    router.replace(`${pathName}?${params.toString()}`);
  };

  return (
    <div
      className={cn(
        "w-full h-full flex flex-row items-center justify-start gap-5",
        {
          "pointer-events-none opacity-50": !searchParams
            .get("query")
            ?.toString(),
        }
      )}
    >
      <button
        onClick={() => {
          handleSearch("5");
        }}
        className="w-[50px] bg-shades-lightgrey p-2 gap-1 h-full flex flex-col items-center justify-center "
      >
        <p className="text-shades-white font-bold text-[20px]">5</p>
        <div
          className={cn(
            "w-[20px] h-[20px] rounded-full border-2 border-shades-cyan",
            {
              "bg-shades-cyan": searchParams.get("limit")?.toString() === "5",
            }
          )}
        ></div>
      </button>
      
    </div>
  );
}
ok so this is the code, the other buttons are 15 30 and infinite (removed them cause discord char limit)
ok..what does it do?
window.history.pushState shallow update the url
oh
thats it?
yeah
and you can remove this on SearchInput
if (query && limit) params.set("search", "true");
    else {
      params.delete("search");
    }
clicking on the limit dosent update anything now though
like it dosent even show that you clicked 5
@Ray and you can remove this on `SearchInput` ts if (query && limit) params.set("search", "true"); else { params.delete("search"); }
i feel like the url dosent seem to be the best place to do a click to search button
but it also seems to be the only place to do this
@Arinji just at the page, jsx export default async function Page({ searchParams, }: { searchParams?: { query?: string; page?: string; limit?: string; search?: string; }; }) { const user = await GetUser(); let notifications = ""; let disableInput = false; let documents: TicketMessageSchemaType[] = []; if (searchParams) { const { query, limit, search, page } = searchParams; if (!query || !limit || !search) { disableInput = false; } else { let currentPage = 0; if (!page || isNaN(parseInt(page))) currentPage = 0; else currentPage = parseInt(page); disableInput = true; const searchResults = await SearchTicketMessage( query, parseInt(limit), currentPage ); console.log(query, parseInt(limit), currentPage, searchResults); if (searchResults.success) { searchResults.document.forEach((message) => { const parsedMessage = TicketMessageSchema.safeParse(message); console.log(parsedMessage); if (parsedMessage.success) { if (parsedMessage.data.deleted.is_deleted && !user.isStaff) return; else documents.push(parsedMessage.data); } }); console.log(documents); } if (documents.length === 0) notifications = "No results found."; else notifications = `Found results.`; disableInput = false; } } `
  return (
    <div className="">
      <div className="">
        <h2 className="">
          Search For Message
        </h2>
        <div
          className={cn(
            "",
            {
              "pointer-events-none ": disableInput,
            }
          )}
        >
          <MessageInput />
          <LimitInput user={user} />
          <SearchInput />
          <Notifier notification={notifications} />
        </div>
        {documents.length !== 0 && (
          <div className="">
            {documents.map((message) => {
              if (message.deleted.is_deleted && !user.isStaff) return null;
              else
                return (
                  <Link
                    key={message._id}
                    href={`/transcripts/${message.ticket_id}`}
                    className=""
                  >
                    <div className="2">
                      <p className="text-[15px] font-medium text-shades-offwhite">
                        Sent By:
                      </p>
                      <p className="text-[15px] font-bold text-shades-white">
                        Username
                      </p>
                    </div>
                    <div className="">
                      <p className="text-[15px] font-medium text-shades-offwhite">
                        Sent On:
                      </p>
                      <p className="text-[15px] font-bold text-shades-white">
                        {formatDate({
                          inputDate: message.timestamp,

                          reverse: true,
                        })}
                      </p>
                    </div>

                    <div className="">
                      {MessageSelector({
                        messageData: message,
                      })}
                    </div>
                  </Link>
                );
            })}
          </div>
        )}
      </div>
    </div>
  );
@Ray where do you render LimitInput
the thing i replied to is the part above the return statement
only one?
only one...
only one what?
not 5, 15, 30?
yea there is 5 15 30, discord wont allow me to send them here cause char limit
its the same code for all 3 though, all of thim have the handleClick
it should work
what version of next?
14.0.4?
ye
it does shallow route
but that causes nothing to update
@Arinji ye
oh you need this
  experimental: {
    windowHistorySupport:true
  }
thats what shallow routing means right? dont update stuff
in next.config.js
oh
one se
one sec
@Arinji thats what shallow routing means right? dont update stuff
it update on client but not making new request
yay it works
thanks so much
which ans to mark :/
yay
thanks a lot
mark yours if Near is not around:noice:
yes :D
quick question @Ray , in 14.1.0 the history thing became stable right?
kk thanks