Haider Ali Anjum
Haider Ali Anjum

Reputation: 847

Not allowed to use async/await while using serverActions in Client component in next.js

Building an infinite scroll in next.js, I am trying to call my serverAction to loadMoreData and using async/await with it to wait for the API call and get result.

Getting an error:

"async/await is not yet supported in Client Components, only Server Components."

page.tsx is a server component and calls the same server action one time for initial images.

// page.tsx

const Home = async ({ searchParams }) => {

  const searchKeyword =
    typeof searchParams.keyword === "string" ? searchParams.keyword : "";
  
  let apiResponse = await getPhotos({ query: searchKeyword });

  return (
    <main className='p-6 flex justify-center w-full min-h-screen bg-black'>
      <InfiniteScrollData
        search={searchKeyword}
        initialData={apiResponse?.results || []}
      />
    </main>
  );
};

export default Home;

Here is the server action:

// actions.ts

"use server";


export const getData = async ({
  query,
}) => {
  
  const { response } = await fetchData({
    query,
  });
  apiResponse = response;
  
  return response;
};

And following is the client component

// InfiniteScrollData.tsx

"use client";

// imports


const InfiniteScrollImages: = ({
  search,
  initialData,
}) => {
  const [data, setData] = useState(initialData);
  const [page, setPage] = useState(1);
  const [loaderRef, inView] = useInView();

  const loadMoreData = useCallback(async () => {
    const next = page + 1;
    const response = await getData({ query: search, page: next });
    if (response?.results?.length) {
      setPage(next);
      setData((prev) => [...prev, ...(response?.results || [])]);
    }
  }, [page, search]);

  useEffect(() => {
    if (inView) {
      loadMoreData();
    }
  }, [inView, loadMoreData]);

  return (
    <>
      <div>
        {photos?.map((item) => (
          <Card key={item.id} data={item} />
        ))}
      </div>
      <div ref={loaderRef}>
        <Loader />
      </div>
    </>
  );
};

export default InfiniteScrollImages;


Using [email protected]

Maybe there is another way of calling server actions from client components?

Upvotes: 0

Views: 2622

Answers (5)

Yilmaz
Yilmaz

Reputation: 49293

there are 2 options to call server actions in client components.

First is using bind

You can bind arguments to a Server Action using the bind method. This allows you to create a new Server Action with some arguments already bound. This is beneficial when you want to pass extra arguments to a Server Action.

Good to know: .bind of a Server Action works in both Server and Client Components. It also supports Progressive Enhancement.

Progressive enhancement allows a to function properly without JavaScript, or with JavaScript disabled.

since you are Calling a Server Action outside of form, u need to use useTransition

When using a Server Action outside of a form, call the Server Action in a transition, which allows you to display a loading indicator, show optimistic state updates, and handle unexpected errors. Forms will automatically wrap Server Actions in transitions.

since we usually navigate the user to somewhere else, startTransition makes sure we redirect after data is changed.

Upvotes: 1

Abdul Rahim
Abdul Rahim

Reputation: 129

You are mixing client components with server components.

Check this link which will give more idea on this : https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#example

Below code will remove the error.

// page.tsx
const Home = ({ searchParams }) => {
  const searchKeyword =
  typeof searchParams.keyword === "string" ? searchParams.keyword : "";
  
    return (
    <main className='p-6 flex justify-center w-full min-h-screen bg-black'>
      <InfiniteScrollData
        search={searchKeyword}
      />
    </main>
  );
};
export default Home;

This component has to load the data. You can't implement infinite loading on server component. That has to be done in client only.

// InfiniteScrollData.tsx

"use client";

// imports


const InfiniteScrollImages: = ({
  search,
  initialData,
}) => {
  let initialData = await getPhotos({ query: searchKeyword });
  const [data, setData] = useState(initialData);
  const [page, setPage] = useState(1);
  const [loaderRef, inView] = useInView();

  const loadMoreData = useCallback(async () => {
    const next = page + 1;
    const response = await getData({ query: search, page: next });
    if (response?.results?.length) {
      setPage(next);
      setData((prev) => [...prev, ...(response?.results || [])]);
    }
  }, [page, search]);

  useEffect(() => {
    if (inView) {
      loadMoreData();
    }
  }, [inView, loadMoreData]);

  return (
    <>
      <div>
        {photos?.map((item) => (
          <Card key={item.id} data={item} />
        ))}
      </div>
      <div ref={loaderRef}>
        <Loader />
      </div>
    </>
  );
};

export default InfiniteScrollImages;

Upvotes: 0

elcharitas
elcharitas

Reputation: 151

Maybe there is another way of calling server actions from client components?

You have two options:

  1. Turn getData to client code - This just requires that getData is moved from the actions file without the "use server" directive and your code should work as expected. But I'm sure you wouldn't want this.

  2. The second is to follow react's docs and call the Server Action in a transition. You would need to modify your loadMoreData handler as below:

const loadMoreData = useCallback(() => {
    startTransition(async () => {
      const next = page + 1;
      const response = await getData({ query: search, page: next });
      if (response?.results?.length) {
        setPage(next);
        setData((prev) => [...prev, ...(response?.results || [])]);
       }
    });
}, [page, search]);

More information is available on react's docs on server actions

Upvotes: 1

floor
floor

Reputation: 1572

As the error suggested async await isn't supported in client components. Your alternative would be to handle promises. Your getData async function returns a promise which you can handle in your useCallback or just return the promise there as well and handle the promise in the useEffect

Without a demo I am kind of guessing so here is a rough solution:

const loadMoreData = useCallback(() => {
    const next = page + 1;
    getData({ query: search, page: next }).then(response => {
      if (response?.results?.length) {
        setPage(next);
        setData((prev) => [...prev, ...(response?.results || [])]);
      }
    }).error(() => {

    });
  }, [page, search]);

On Nextjs documentation for client data fetching this is the example they give:

import { useState, useEffect } from 'react'

  function Profile() {
    const [data, setData] = useState(null)
    const [isLoading, setLoading] = useState(true)

    useEffect(() => {
      fetch('/api/profile-data')
        .then((res) => res.json())
        .then((data) => {
          setData(data)
          setLoading(false)
        })
    }, [])

    if (isLoading) return <p>Loading...</p>
    if (!data) return <p>No profile data</p>

    return (
      <div>
        <h1>{data.name}</h1>
        <p>{data.bio}</p>
      </div>
    )
  }

Upvotes: 0

Markiian Benovskyi
Markiian Benovskyi

Reputation: 2161

Your error directly says that you cannot do await functions in client components, so you should find away to get rid of that in your code:

const Home = ({ searchParams }) => { // no async 

Upvotes: -2

Related Questions