Next.js Discord

Discord Forum

Suspense + RSC + API routes - Single query runs 5 times?!

Answered
Barbary Lion posted this in #help-forum
Open in Discord
Barbary LionOP
Hey, this is my first big Next.js project and I tried to use Suspense with React Server Component and NextJS API routes.
I don't know what I'm doing wrong, but my RSC loads 5 times even with hardcoded props with 5 call to the API route.
If it was only 2, I'd probably blame the Strict Mode, but 5?
Answered by aardani
@Barbary Lion i recommend not putting suspense in a client component
View full answer

21 Replies

Barbary LionOP
Parent component:
    return (
        <div className="w-full">
            <Suspense fallback={<div className="text-2xl">Loading...</div>}>
                <SearchResult query={'Bombtrack'} />
            </Suspense>
        </div>
    );
RSC Component:
export default async function SearchResult({ query }: SearchResultProps) {
    if (!query) return null;
    console.log(query);

    const req = await fetch(`api/spotify/searchSong`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ search: query })
    });
    const data = await req.json();

    if (!data) return null;

    const results = await data.tracks.items;

    return (
        <div className="flex flex-col gap-2">
            {results.map((i) => (
                <SearchResultItem key={i.id} data={i} />
            ))}
        </div>
    );
}
API route (app/api/spotify/searchSong/route.ts):
export async function POST(request: NextRequest) {
    const session = await getServerSession();
    if (!session) return new NextResponse('Unauthorized', { status: 401 });

    const token = await getSpotifyToken();

    const { search } = await request.json();

    if (!token) return NextResponse.json({ error: 'Error while getting Spotify token' });
    console.log('request');
    const spotifyRequest = await fetch(
        `https://api.spotify.com/v1/search?q=${encodeURI(search)}&type=track&limit=10`,
        {
            headers: {
                Authorization: `Bearer ${token}`
            }
        }
    );
    const data = await spotifyRequest.json();
    return NextResponse.json(data);
}
Server console:
request
request
request
request
request

Browser/Client console:
Bombtrack
Bombtrack
Bombtrack
Bombtrack
Bombtrack
(I hardcoded the props to Bombtrack just to elimite possible issues with the props)
Barbary LionOP
I tried wrapping the RSC using {children} method but it still fires multiple times
@Barbary Lion can you send full code for Parent component?
@aardani <@327969147671674880> can you send full code for Parent component?
Barbary LionOP
Sorry for late answer
'use client';

import { Suspense, useState } from 'react';
import * as z from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';

import {
    Form,
    FormControl,
    FormField,
    FormItem,
    FormLabel,
    FormMessage
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import SearchResult from '@/components/search/SearchResult';

export function SearchSong() {
    // const [query, setQuery] = useState<string | null>(null);

    const formSchema = z.object({
        songname: z.string().min(1, {
            message: 'Search term must be at least 1 character long.'
        })
    });

    type Schema = z.infer<typeof formSchema>;

    const form = useForm<Schema>({
        resolver: zodResolver(formSchema),
        defaultValues: {
            songname: ''
        }
    });

    const onSubmit = (formData: Schema) => {
        // setQuery(formData.songname);
        console.log(formData); // disabled for now
    };
// part 1
// part 2
    return (
        <div className="w-full">
            <Form {...form}>
                <form onSubmit={form.handleSubmit(onSubmit)} className="w-full flex py-5 gap-5">
                    <FormField
                        control={form.control}
                        name="songname"
                        render={({ field }) => (
                            <FormItem className="flex items-center gap-2 flex-grow">
                                <FormLabel>Spotify</FormLabel>
                                <FormControl className="">
                                    <Input placeholder="Type your song..." {...field} />
                                </FormControl>
                                <FormMessage />
                            </FormItem>
                        )}
                    />
                    <Button type="submit">Submit</Button>
                </form>
            </Form>
            <Suspense fallback={<div className="text-2xl">Loading...</div>}>
                <SearchResult query={'Bombtrack'} />
            </Suspense>
        </div>
    );
}
Some leftovers from some tentatives 😅
I could not work on it today, but my next try was going to be: remove completely the React Hook Form, I noticed some strange behaviour (loading appearing when I start to type)
and to be fair it's quite overkill for a simple search bar.. but it was included with the shadcn/form component that I meant to use originally with a POST to API route (thus the Form seemed like a good option) before going to a RSC (setQuery setting the props for the RSC child)
@Barbary Lion i recommend not putting suspense in a client component
Answer
every compeontn thats imported to a "use client" file will be converted/acted as a client component
and client component can't be async (SearchResult)
therefore you either have 2 option
ilft up the. {children} prop to pass servercomponent inside SearchSong
or
convert SearchResult into client components only
using async in client components is not yet supported now and will produce unexpected result
@aardani <@327969147671674880> i recommend not putting suspense in a client component
Barbary LionOP
Thanks for the suggestions. It fixed it 💪