Dynamically Render UI Based on User Session with SSR in Next.js Client Components

Jon Meyers
InstructorJon Meyers
Share this video with your friends

Social Share Links

Send Tweet

The first render of a Client Component happens on the server. This is called Server-side rendering (SSR). However, Client Components are synchronous, so they cannot suspend rendering while fetching data - such as the user's session. This means we either need to display a loading spinner or render the logged out state, while fetching that async data, causing a flash of incorrect state for the user.

In this lesson, we look at rendering a Client Component from a Server Component that can asynchronously fetch the user's Supabase session, and pass it as a prop to the Client Component, making the user's session available on its first SSR render.

Code Snippets

Render Client Component from Server Component

const {
  data: { session },
} = await supabase.auth.getSession();

return <ClientComponent session={session} />;

Resources

Instructor: Our application has buttons for the user to log in and log out. If they are logged out, it probably doesn't make sense for them to see this button anymore. They should probably only see the Login button. When they log in, they should only see the Logout button as this is the only one that's relevant to their current state.

Let's make these buttons aware of our user's session so we know which one to show. If the user does have a session, then we want to show them the Logout button, and then if they don't have a session, then we want to show the Login button. We can get rid of this fragment.

Where do we get this session? One way we can do this is by using useState to create a variable for our session. We can then use useEffect to run some logic when our component mounts. We need to do some async stuff here. We're going to declare a new function called getSession. This is going to be an async function which gets some data by awaiting a call to supabase.auth.getSession.

Now, we want to call our getSession function that we declared above and update our session variable to be that session that we got back from supabase. Let's not worry about TypeScript for now because we're not going to use this block of code, but we need to make sure we call that getSession function.

Now, if we go back to the browser, we'll see our Logout button. This looks like it's all working. Let's click Logout. Our list of tweets is now an empty array, but our button still says Logout. Let's try refreshing. OK, it says Login. That's good. Now let's click Login. That seems to have updated our tweets and our button, but when we refresh, we see this flash of the Login button.

This is because this session variable is undefined on the first render of our application which happens on the server. Even though our user should be authenticated and seeing the Logout button, the session is undefined, so they see Login instead. Now when this page is loaded in the browser, this will then call useEffect.

This calls the getSession function which goes off to supabase and gets a session. Then we update our local session, which then causes this component to re-render, only this time there is a session, so it displays the Logout button. Seeing that flash of the Login button is not a very good user experience.

How can we make this session variable available and correct on that initial server rendered version of our component? Well, if we have a look at the server component for our learning page, it can be an async function. Just suspend the rendering of our component until this promise from supabase resolves.

Let's wrap this client component in an async server component that can fetch that session from supabase before the initial render, and then pass that session down to our client component, which will then have the correct state for our session on its first render.

Let's create a new file called auth-button-server.tsx. This -server part is not required, it's just to help us easily distinguish between our server and client component. We could rename this one to be -client. Now, in our auth-button-server component, we want to export a default async function for our AuthButtonServer component.

In here, we're going to need a new supabase client which we can get by calling the createServerComponentClient function which comes in from our auth-helpers package. We then need to pass it our cookies function which we can import from next/headers. Now we can move that call to supabase to get our user session from our client component across to our server component.

I'm going to destructure the session object directly here. We can then clean up our client component by removing useEffect, and now useState variable, and then this session can come in from our component's props. We're going to pass it a session.

We can type this correctly to say our session is going to either be of type Session, which comes in from our auth-helpers package, or it's going to be null, if our user is not signed in. We can now get rid of that import for useEffect and useState.

We could also update the name of our function to be AuthButtonClient, which makes it super clear over in our server component when we want to return our AuthButtonClient component, which comes in from ./auth-button-client. If we close this one off, it's going to tell us that we need to pass it a session. We can pass it a prop for session, which is that session that we got from supabase.

Back over in page.tsx, we want to import our AuthButtonServer from auth-button-server, which is then what we want to render from our component. Now, if we go back to the browser, we see the correct Logout button.

When we refresh, we can see that our Logout button is stable. We don't see that flash of Login. If we log out, we see our Login button. When we log in, our tweets and our Logout button are updated.

To quickly recap, we wanted to dynamically render a different piece of UI from our client component based on the user's session. Since client components are synchronous, there was no way to fetch the user's session before the initial render of our component, which happens server-side.

Since server components can be async, we first fetched our user's session from supabase, and then passed that to our client component as the session prop, which made it available on the first render of our client component.