Loading state for Pagination
Answered
Arinji posted this in #help-forum
ArinjiOP
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.
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("")
})
}93 Replies
ArinjiOP
hi ray
@Arinji 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.
const [isPending, startTransition] = useTransition()
const router = useRouter()
const handleClick = () => {
startTransition(() => {
router.push("")
})
}Answer
ArinjiOP
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
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
then
startTransition(() => {
router.replace(`${pathName}?${params.toString()}`);
})isPending will be the loading state
ArinjiOP
Ohh ok one sec
lemme try
and remove all of the useEffect
ArinjiOP
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
ArinjiOP
All good now?
yes
ArinjiOP
oki lemme test
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?
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");
}ArinjiOP
uhh in the search component right?
search button
in side handleSearch
ArinjiOP
oki one sec
oh wait
ArinjiOP
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?ArinjiOP
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
ArinjiOP
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?
ArinjiOP
yes, so click the button once, then it shows the data and then dosent show again till you click the button again
@Arinji but since i dont ever set search to false, if i click on 30 or 5.. it re renders the data
the code for button 5,15,30?
ArinjiOP
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)
@Arinji jsx
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>
);
}
const handleSearch = (limit: string) => {
const params = new URLSearchParams(searchParams);
if (limit) {
params.set("limit", limit);
} else {
params.delete("limit");
}
window.history.pushState(null, "", `?${params.toString()}`)
};ArinjiOP
ok..what does it do?
window.history.pushState shallow update the url
ArinjiOP
oh
thats it?
yeah
and you can remove this on
SearchInputif (query && limit) params.set("search", "true");
else {
params.delete("search");
}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");
}
ArinjiOP
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 i feel like the url dosent seem to be the best place to do a click to search button
where do you render LimitInput
@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;
}
}
`
ArinjiOP
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
ArinjiOP
the thing i replied to is the part above the return statement
only one?
ArinjiOP
only one...
only one what?
not 5, 15, 30?
ArinjiOP
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
@Arinji ye
oh you need this
experimental: {
windowHistorySupport:true
}ArinjiOP
thats what shallow routing means right? dont update stuff
in next.config.js
@Arinji thats what shallow routing means right? dont update stuff
it update on client but not making new request
ArinjiOP
yay
thanks a lot
mark yours if Near is not around

ArinjiOP
yes :D
ArinjiOP
quick question @Ray , in 14.1.0 the history thing became stable right?
ArinjiOP
kk thanks