Search code examples
javascriptreactjsnext.jsapp-routernext.js14

Failing to fetch data inside a client component


I'm currently working on a hobby project, developing an odds comparison website using Next.js for the frontend and Django REST Framework along with Django Channels for the backend. Despite reading through the Next.js documentation multiple times, I'm struggling to piece everything together effectively. Here's the current structure of my project:

├───app
│   │   layout.tsx
│   │
│   └───odds
│       └───[sport]
│           └───[country]
│               └───[competition]
│                       page.tsx
│
└───components
        Sidebar.tsx

In layout.tsx, I'm fetching initial data to populate the sidebar::

// layout.tsx

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const initialData = await fetch("http://127.0.0.1:8000/api/", { next : { revalidate: 10}});
  const data = await initialData.json();

  return (
    <html>
      <body>
         <div>
          <Sidebar initialData={data} />
         </div>
           {children}
      </body>
    </html>
  );
}

In page.tsx, I'm attempting to fetch data to display the initial odds, alongside establishing a WebSocket connection for real-time updates:

// page.tsx

'use client'

async function fetchData() {
  const res = await fetch("http://127.0.0.1:8000/api/", {
    cache: 'no-store',
  });
  return await res.json()
}

export default async function Main({
  params,
}: {
  params: { sport: string; country: string; competition: string };
}) {

  const data = await fetchData()
  const [newOdds, setOdds] = useState(data);

  useEffect(() => {
    let ws = new WebSocket("ws://127.0.0.1:8000/ws/odds/");
    ws.onopen = () => {
      ws.send(JSON.stringify({ connection: "start" }));
    };
    ws.onmessage = (e) => {
      const message = JSON.parse(e.data);
      setOdds(message);
    };

    return () => {
      ws.close();
    };
  }, []);

  return (
      ...
)
}

However, I'm encountering a problem where the server gets overwhelmed with http requests that never cease (regardless of cache settings) and the page never renders. It seems like the issue might be related to where fetchData() is being called. I am wondering what would be the best practice in this scenario? I ideally want to pass data from the layout to children, but this isn't directly supported, and the workarounds I've found seem somewhat hacky. Any insights or suggestions would be greatly appreciated.


Solution

  • You can't fetch directly in a Client component like you do in page.tsx.

    Your Main function in your page.tsx should not be async (and you already probably have a warning from TSLint/ESLint in your IDE). Your code makes an infinite loop, and it's probably due to that.

    A simple way to solve this would be to make your fetchData once, in a useEffect, and then setOdds(data) inside.

    That would be the basics, but you will then need to handle errors, loading states, etc... Thats's why we usually use libs like react-query , if you want to make these kinds of fetching client-side.

    Another way, which is cleanier in the new Next.js + Server Components paradigm, would be to make your fetchData() in a server component (just like you did in your layout), and send this data to a child component, which will have the "use client" directive, and could handle all the websocket stuff.

    A quick (untested) example :

    // page.tsx
    
    async function fetchData() {
      const res = await fetch("http://127.0.0.1:8000/api/", {
        cache: 'no-store',
      });
      return await res.json()
    }
    
    export default async function Main({
      params,
    }: {
      params: { sport: string; country: string; competition: string };
    }) {
    
      const data = await fetchData() 
    
      return (
          <ChildClientComponent data={data} params={params} />
      )
    }
    
    // ChildClientComponent.tsx
    
    'use client'
    
    export default function ChildClientComponent(
      data,
      params
    }) {
    
      const [newOdds, setOdds] = useState(data);
    
      useEffect(() => {
        let ws = new WebSocket("ws://127.0.0.1:8000/ws/odds/");
        ws.onopen = () => {
          ws.send(JSON.stringify({ connection: "start" }));
        };
        ws.onmessage = (e) => {
          const message = JSON.parse(e.data);
          setOdds(message);
        };
    
        return () => {
          ws.close();
        };
      }, []);
    
      return (
          ...
    )
    }