How to call a route handler from a client component? (App Router)
Answered
African Slender-snouted Crocodilโฆ posted this in #help-forum
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
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
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
36 Replies
![Avatar](https://cdn.discordapp.com/avatars/657067112434499595/c78dd8e985ede1ad50c31ac682dd91a1.webp?size=256)
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
but id ask you if you really need that and if prerender the data could be better
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
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](https://cdn.discordapp.com/avatars/657067112434499595/c78dd8e985ede1ad50c31ac682dd91a1.webp?size=256)
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](https://cdn.discordapp.com/embed/avatars/2.png)
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](https://cdn.discordapp.com/avatars/125261181458972672/dea2a3bdbd363e71eef86be7728cc712.webp?size=256)
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
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](https://cdn.discordapp.com/avatars/125261181458972672/dea2a3bdbd363e71eef86be7728cc712.webp?size=256)
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](https://cdn.discordapp.com/embed/avatars/2.png)
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.
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](https://cdn.discordapp.com/embed/avatars/2.png)
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
And then from the docs here:
https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating
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](https://cdn.discordapp.com/avatars/125261181458972672/dea2a3bdbd363e71eef86be7728cc712.webp?size=256)
Julienng
Well, I would argue you are doing a worse cache of your request anyway using
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
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](https://cdn.discordapp.com/embed/avatars/2.png)
African Slender-snouted CrocodileOP
so maybe it makes sense to use either TanStack Query / SWR after all? ๐
![Avatar](https://cdn.discordapp.com/avatars/125261181458972672/dea2a3bdbd363e71eef86be7728cc712.webp?size=256)
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](https://cdn.discordapp.com/embed/avatars/2.png)
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
Here I'm thinking; let next.js handle this using error.tsx
![Avatar](https://cdn.discordapp.com/avatars/125261181458972672/dea2a3bdbd363e71eef86be7728cc712.webp?size=256)
Julienng
Well, looking at mine, there is still quite a few mistakes honestly ๐
handling promise correctly really not that easy
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
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](https://cdn.discordapp.com/avatars/125261181458972672/dea2a3bdbd363e71eef86be7728cc712.webp?size=256)
Julienng
The response needs to be from the React core team (there are unstable proposal like a
The default is clearly react-query / swr for now until we have something baked in react
use(promise)
)The default is clearly react-query / swr for now until we have something baked in react
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
African Slender-snouted CrocodileOP
SWR seems more popular
![Avatar](https://cdn.discordapp.com/avatars/125261181458972672/dea2a3bdbd363e71eef86be7728cc712.webp?size=256)
Julienng
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
African Slender-snouted CrocodileOP
ah, checking...
hm they're both soaring like crazy
![Avatar](https://cdn.discordapp.com/avatars/125261181458972672/dea2a3bdbd363e71eef86be7728cc712.webp?size=256)
Julienng
Yeah, crazy popular solutions right now
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
African Slender-snouted CrocodileOP
which one has better docs in your opinion?
![Avatar](https://cdn.discordapp.com/avatars/125261181458972672/dea2a3bdbd363e71eef86be7728cc712.webp?size=256)
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
Both will be great, check both docs and usage of useQuery and settle on what clicks for you
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
African Slender-snouted CrocodileOP
I really liked the react query devtool extension but I went with SWR ๐
![Avatar](https://cdn.discordapp.com/avatars/657067112434499595/c78dd8e985ede1ad50c31ac682dd91a1.webp?size=256)
risky
Swr has one too ๐
![Avatar](https://cdn.discordapp.com/embed/avatars/2.png)
African Slender-snouted CrocodileOP
@risky yep, got it installed. It's not as fancy as TanStack Query's but it's still very useful.