Next.js Discord

Discord Forum

How to call a route handler from a client component? (App Router)

Answered
African Slender-snouted Crocodil… posted this in #help-forum
Open in Discord
Avatar
African Slender-snouted CrocodileOP
// app/api/hello/route.js import { NextResponse } from 'next/server'; export async function GET() { return NextResponse.json({ message: 'Hello, world!' }); } // components/ClientComponent.js 'use client'; // Use this directive to mark this as a client component import React, { useEffect, useState } from 'react'; const ClientComponent = () => { const [data, setData] = useState(null); useEffect(() => { const fetchData = async () => { try { const response = await fetch('/api/hello'); const result = await response.json(); setData(result); } catch (error) { console.error('Error fetching data:', error); } }; fetchData(); }, []); return ( <div> {data ? <p>{data.message}</p> : <p>Loading...</p>} </div> ); }; export default ClientComponent; // app/page.js import ClientComponent from '../components/ClientComponent'; export default function Home() { return ( <div> <h1>Welcome to Next.js 14 with the App Router!</h1> <ClientComponent /> </div> ); }

I checked https://nextjs.org/docs/app/building-your-application/routing/route-handlers but there are 0 examples on how to call a route handler from a client component.

Can someone please share some best practices? Is using useEffect, and calling the route handler from there, the only way in Next.js 14?
Answered by Julienng
Hi,

You can add code using three backlit ```


I highly recommend you to move from fetch in useEffect from something like react-query or swr.

I've made three fetch pattern example yesterday here: https://codesandbox.io/p/devbox/wispy-butterfly-dcddqw

To find the fetch pattern:
- folder p1 : server only pattern
- folder p2: server start fetching, client component resolve the fetch
- folder p3: route handler and fetch from the client
View full answer

36 Replies

Avatar
Risky
useEffect can be looked down upon compared to swr/react query because it can fetch more then needed (ie dev mode strict react does useEffect twice),

but id ask you if you really need that and if prerender the data could be better
Avatar
African Slender-snouted CrocodileOP
i cant prerender/fetch the data in a server component because, what is being fetched is dynamic and the fetch will be triggered on an onClick event in a client component. Or what did you have in mind exactly?
Avatar
Risky
yeah that makes sense, i was just thinking lots put their own "fancy" api when you can just use server component instead... for you i can see the value here (button doesnt need useEffect tho)
i personally use RSC fetch and SWR when i needed route hadler and client
Avatar
African Slender-snouted CrocodileOP
I ended up with the following solution; seems to be working and I think the fetchCup in route.ts is cached by default?
import { fetchCup } from "@/components/cups/requests/requests"; import { NextRequest } from "next/server"; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const slugsQuery = searchParams.get("slugs"); const slugs = slugsQuery ? slugsQuery.split(",") : []; const cups = await Promise.all(slugs.map((slug) => fetchCup(slug))); const validCups = cups.filter((cup) => cup !== undefined); return Response.json(validCups); }
and from my client component I call the route handler endpoint:

useEffect(() => { const fetchCupsData = async () => { const slugs = selectedCups.map((cup) => cup.slug); const queryParams = new URLSearchParams({ slugs: slugs.join(","), }); const response = await fetch(/api/cups?${queryParams.toString()}); if (!response.ok) { throw new Error(Failed to fetch cups. Status: ${response.status} ${response.statusText}, ); } const result = await response.json(); if (!result || !Array.isArray(result)) { throw new Error("Unexpected response format."); } setCupsData(result); }; fetchCupsData(); }, [selectedCups]);
Avatar
Julienng
Hi,

You can add code using three backlit ```


I highly recommend you to move from fetch in useEffect from something like react-query or swr.

I've made three fetch pattern example yesterday here: https://codesandbox.io/p/devbox/wispy-butterfly-dcddqw

To find the fetch pattern:
- folder p1 : server only pattern
- folder p2: server start fetching, client component resolve the fetch
- folder p3: route handler and fetch from the client
Answer
Avatar
Julienng
For example, your current code under a react-query example:

function fetchCupsData(slugs) {
  const queryParams = new URLSearchParams({
    slugs: slugs.join(","),
  });
  const response = await fetch(/api/cups?${queryParams});

  if (!response.ok) {
    throw new Error(
    `Failed to fetch cups. Status: ${response.status} ${response.statusText}`,
    );
      }

  const result = await response.json();
  
  if (!result || !Array.isArray(result)) {
    throw new Error("Unexpected response format.");
  }
  return result;
}

function MyComponent() {
  const {data, isLoading} = useQuery({
    queryKey: ['cups', slugs] // i don't know where slugs came from
    queryFn: () => fetchCupsData(slugs)
  });
}
Avatar
African Slender-snouted CrocodileOP
what would be the benefit of using react-query or SWR instead of fetch in a route handler? According to the docs route handlers are cached by default.

From the next.js docs:

Fetching Data on the Client with third-party libraries
You can also fetch data on the client using a third-party library such as SWR or TanStack Query. These libraries provide their own APIs for memoizing requests, caching, revalidating, and mutating data.

I do not need revalidating, mutating data.

I must admit I do not like useEffect but in this particular case is gets the job done and it's not that difficult to understand what it does.

But again, I'm open to suggestions/better/simpler solutions.

One cons is that I would need to learn another library and last time I played around with react-query I did not find the DX to be that good. SWR I've never used.
The code snippet you pasted does not look too complicated.
But I would be introducing more bytes to the client bundle with either of those libraries
That's also something to take into consideration
TBH what bugs me the most is that I have to use useEffect 😄
Avatar
African Slender-snouted CrocodileOP
And actually now I'm a bit confused....

From the docs here:
https://nextjs.org/docs/app/building-your-application/routing/route-handlers
Route Handlers are cached by default when using the GET method with the Response object.

And then from the docs here:
https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating
In Route handlers, fetch requests are not memoized as Route Handlers are not part of the React component tree.
Avatar
Julienng
Well, I would argue you are doing a worse cache of your request anyway using useState 😄

Without a joke, in the current useEffect, you have a few bugs / ux inconsistencies
- no loading state
- no error state
- race condition if the component re-render (longer fetch response from server win)
- no dedup of request if the component is render multiple times
Avatar
African Slender-snouted CrocodileOP
so maybe it makes sense to use either TanStack Query / SWR after all? 😄
Avatar
Julienng
The useEffect should look more like:

import { flushSync } from 'react-dom';

export function useQuery() {
  const [isLoading, setIsloading] = useState(false);
  const [data, setData] = useState();
  const [error, setError] = useState(null);

  useEffect(() => {
    let ignore = false;

    const fetchCupsData = async () => {
      const slugs = selectedCups.map(cup => cup.slug);
      const queryParams = new URLSearchParams({
        slugs: slugs.join(',')
      });

      const response = await fetch(`/api/cups?${queryParams}`);

      if (!response.ok) {
        throw new Error(`Failed to fetch cups. Status: ${response.status} ${response.statusText}`);
      }

      const result = await response.json();

      if (!result || !Array.isArray(result)) {
        throw new Error('Unexpected response format.');
      }

      if (!ignore) {
        setCupsData(result);
        setError(null);
      }
    };

    // flushSync to show user a feedback
    flushSync(() => setIsloading(true));
    fetchCupsData()
      .catch(err => {
        // do I want to clean data?
        // how do I get previousData?
        setError(err);
      })
      .finally(() => {
        setIsloading(false);
      });

    return () => {
      ignore = true;
    };
  }, [selectedCups]);

  return { isLoading, data, error };
}
And i'm not handling deduplication, previousData, pagination, etc
One of the maintainer of react-query have a lot of resources about react-query, one of which is talking about the subject here: https://tkdodo.eu/blog/why-you-want-react-query
Avatar
African Slender-snouted CrocodileOP
Thanks for pointing out all the flaws in my code. I appreciate it!
Let me start with no error state;

Here I'm thinking; let next.js handle this using error.tsx
Avatar
Julienng
Well, looking at mine, there is still quite a few mistakes honestly 😅 handling promise correctly really not that easy
Avatar
African Slender-snouted CrocodileOP
there's a lot of what i would call boilerplate code in your example; that's one of the reasons I created this question in the first place... thinking that maybe nextjs has something clever to simplify all of this... like with const data = await fetch() in a server component. Easy (or easier/less code). With a client component... a totally different story... lots of code for one fetch 😦
Avatar
Julienng
The response needs to be from the React core team (there are unstable proposal like a use(promise))

The default is clearly react-query / swr for now until we have something baked in react
Avatar
African Slender-snouted CrocodileOP
SWR seems more popular
Avatar
Julienng
The last few version are under @tanstack scope
https://npmtrends.com/@tanstack/react-query-vs-swr
Avatar
African Slender-snouted CrocodileOP
ah, checking...
hm they're both soaring like crazy
Avatar
Julienng
Yeah, crazy popular solutions right now
Avatar
African Slender-snouted CrocodileOP
which one has better docs in your opinion?
Avatar
Julienng
Well, I've never used swr in prod, so my opinion is probably biased here

Both will be great, check both docs and usage of useQuery and settle on what clicks for you
Avatar
African Slender-snouted CrocodileOP
I really liked the react query devtool extension but I went with SWR 👍
Avatar
Risky
Swr has one too 👀
Avatar
African Slender-snouted CrocodileOP
@Risky yep, got it installed. It's not as fancy as TanStack Query's but it's still very useful.