How I order these Next.js components so Next, Ably and Clerk work in tandem?

217 views Asked by At

I am currently trying to set up an app with Clerk (authentication) and Ably (WebSockets for realtime communication), but I can't figure out how to architect this where one piece doesn't break another. I can't get Next.js, Ably, and Clerk to all work together.

The Component Tree in the render looks like this:

  <ClerkProvider>
    <AblyProvider client={client}>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </AblyProvider>
  </ClerkProvider>

The problem is that I want Clerk to wrap Ably, because I want to prevent the client from being able to get a token from Ably until they're authenticated. But they all render, and Ably is trying to get a token before Clerk has the user signed in, it sends the request, and then the request times out after 10 seconds.

So I tried to get user info, and prevent the client from being instantiated until useUser().isSignedIn === true . But then I realized that I can't get access to useUser() at all because it can't be used outside of ClerkProvider. Since we're already in _app.tsx, there's not really a way to go out a layer and wrap things.

So, is there some sort of way to delay the instantiating of client for Ably until Clerk finishes? I can't move client farther down, because then we're in the territory of reloading components, and I don't want to open and close connections to Ably repeatedly. To prevent getting charged obscene amount for concurrent connections that haven't dropped yet, I have to put client for Ably in the root file.

Any ideas here?

This is what I had arrived at before I realized I was trying to call a context outside of its provider and none of this was going to work:

import '@/styles/globals.scss';
import type { AppProps } from 'next/app';
import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';
import { useEffect } from 'react';
import { ClerkProvider, useUser } from '@clerk/nextjs';
import Layout from '@/components/layout/Layout';

let client;

export default function App({ Component, pageProps }: AppProps) {
  const user = useUser();
  const isSignedIn = user.isSignedIn;
  useEffect(() => {
    if (!isSignedIn) return;
    client = new Ably.Realtime.Promise({
      authUrl: 'http://localhost:3000/api/createTokenRequest',
    });
    const handleBeforeUnload = () => {
      client.connection.close();
    };
    window.addEventListener('beforeunload', handleBeforeUnload);
    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, [isSignedIn]);
  return (
    <ClerkProvider
      appearance={{
        baseTheme: 'dark',
        layout: {
          socialButtonsPlacement: 'bottom',
        },
        variables: { colorPrimary: '#000000' },
        elements: {
          'cl-formButtonPrimary':
            'bg-black border border-black border-solid hover:bg-white hover:text-black',
          socialButtonsBlockButton:
            'bg-white border-gray-200 hover:bg-transparent hover:border-black text-gray-600 hover:text-black',
          socialButtonsBlockButtonText: 'font-semibold',
          formButtonReset:
            'bg-white border border-solid border-gray-200 hover:bg-transparent hover:border-black text-gray-500 hover:text-black',
          membersPageInviteButton:
            'bg-black border border-black border-solid hover:bg-white hover:text-black',
          card: 'bg-[#fafafa]',
        },
      }}
      {...pageProps}
    >
      <AblyProvider client={client}>
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </AblyProvider>
    </ClerkProvider>
  );
}

I'm not quite sure how to make all these pieces and these requirements work together. Someone please help. Surely that has to be some kind of React or Next feature I'm not schooled in yet that can solve something like this.

1

There are 1 answers

0
ablydevin On BEST ANSWER

I suspect whats happening is the Javascript that creates the instance of the Ably client is running before the JSX is returned by your component, and therefore before Clerk is able to check for route authentication.

I'd suggest you create a new child component that includes the <AblyProvider> like this:

<ClerkProvider>
  <RealtimeChildComponent />
</ClerkProvider>

Then in <RealtimeChildComponent> you can create the instance of the Ably client and pass it to the <AblyProvider>:

'use client'
import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';
export default function RealtimeChildComponent() {
  const client = new Ably.Realtime.Promise({ authUrl: '/api' })
  return (
    <AblyProvider client={ client }>
      {/* Child React components go here. */}
    </AblyProvider>
  )
}

One important thing to note. You should make sure to import (or whatever component has the <AblyProvider> in it) using lazy loading to prevent Next.js from trying to render it on the server.

const RealtimeChildComponent = dynamic(() => import('./RealtimeChildComponent'), {
  ssr: false,
})

Hope that helps.