Next.js Discord

Discord Forum

createContext requires 'use client';, but supabase requires 'use server';

Unanswered
Northern snakehead posted this in #help-forum
Open in Discord
Northern snakeheadOP
I'm trying to make a sessionContext, which listens to auth events.
sessionContext.tsx:
"use client";

import * as React from "react";

import { createClient } from "@/utils/supabase/server";

import { Session } from "@supabase/supabase-js";

// First a session context has to be created to listen to the auth changes, ref - https://supabase.com/docs/reference/javascript/auth-onauthstatechange (use React context for the user session).
const SessionContext = React.createContext<Session | null>(null);

export const SessionProvider = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const [session, setSession] = React.useState<Session | null>(null);

  React.useEffect(() => {
    const monitorAuthState = async () => {
      const supabase = await createClient();

      const {
        data: { subscription },
      } = supabase.auth.onAuthStateChange((event, session) => {
        setSession(session);
      });

      return () => {
        subscription?.unsubscribe();
      };
    };

    monitorAuthState();
  }, []);

  return (
    <SessionContext.Provider value={session}>
      {children}
    </SessionContext.Provider>
  );
};

export const useSessionContext = () => {
  const context = React.useContext(SessionContext);

  return context;
};

createContext only works on client components, but the monitorAuthState func contains supabase, which works only on server components. I tried to add "use server"; inside the same func, but it's showing an error. Can anyone kindly guide how to fix this?

13 Replies

American black bear
you cannot use the supabase client in client components
to fix this create a server action that does the thing and and use it instead
American black bear
another thing is you should probably create a single supabase client instance, export it, and use it throughout your application instead of creating a new connection every time you use supabase.

with this approach you will probably be able to do subscription thing without the use of server actions or "use server" as you've mentioned
@American black bear another thing is you should probably create a single `supabase` client instance, export it, and use it throughout your application instead of creating a new connection every time you use supabase. with this approach you will probably be able to do `subscription` thing without the use of server actions or "use server" as you've mentioned
Northern snakeheadOP
Yes, I've created the - server.ts, client.ts and middleware.ts files for supabase instance and exported them.
Also, in the auth.ts, I've used the - "use serve";, and created a func named - monitorAuthState():
export async function monitorAuthState() {
  const supabase = await createClient();

  const {
    data: { subscription },
  } = supabase.auth.onAuthStateChange((event, session) => {
    if (event === "SIGNED_IN") {
      return session;
    } else if (event === "SIGNED_OUT") {
      return null;
    }
  });

  return () => {
    subscription?.unsubscribe();
  };
}

I just wanted to ask that, should I use this func in the SessionContext(which is shared above)?
I'm getting an err in here, just trying to debug this:
Argument of type '(event: AuthChangeEvent, session: Session | null) => Session | null | undefined' is not assignable to parameter of type '(event: AuthChangeEvent, session: Session | null) => void | Promise<void>'.
  Type 'Session | null | undefined' is not assignable to type 'void | Promise<void>'.
    Type 'null' is not assignable to type 'void | Promise<void>'.ts(2345)
(parameter) session: Session | null

on line - supabase.auth.onAuthStateChange((event, session) => {
@American black bear you cannot use the supabase client in client components
Northern snakeheadOP
Hey, I got this working!
I now imported supabase from client.ts and used it like this:
"use client";

import * as React from "react";

import { Session } from "@supabase/supabase-js";

import { createClient } from "@/utils/supabase/client";

// First a session context has to be created to listen to the auth changes, ref - https://supabase.com/docs/reference/javascript/auth-onauthstatechange (use React context for the user session).
const SessionContext = React.createContext<Session | null>(null);

export const SessionProvider = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const [session, setSession] = React.useState<Session | null>(null);

  React.useEffect(() => {
    const supabase = createClient();

    const fetchSession = async () => {
      const { data } = await supabase.auth.getSession();
      setSession(data.session);
    };

    fetchSession(); // Call the function

    const { data: authListener } = supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session);
    });

    return () => {
      authListener?.subscription?.unsubscribe();
    };
  }, []);

  return (
    <SessionContext.Provider value={session}>
      {children}
    </SessionContext.Provider>
  );
};

export const useSessionContext = () => {
  const context = React.useContext(SessionContext);

  return context;
};

Now, it's working fine 👍
Thanks
Chilean jack mackerel
Just a tip btw, you should initialize your client on client.ts like this:

import { createBrowserClient } from "@supabase/ssr"
import { SupabaseClient } from "@supabase/supabase-js"
import { Database } from "./types"


export default function createClient(key?: string): SupabaseClient<Database> {
  return createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    (key || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)!
  )
}


And server.ts:
import { cookies } from "next/headers"

import { createServerClient } from "@supabase/ssr"
import { SupabaseClient } from "@supabase/supabase-js"
import { Database } from "./types"


export default async function createClient(key?: string): Promise<SupabaseClient<Database>> {
  const cookieStore = await cookies()

  const isServiceClient = key === process.env.SUPABASE_SERVICE_KEY

  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    (key || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)!,
    {
      cookies: {
        getAll() {
          if (isServiceClient)
            return []
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          if (isServiceClient)
            return
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  )
}


With types.ts being generated with: npx supabase gen types --lang=typescript --project-id YOUR_PROJECT_ID_HERE > utils/supabase/types.ts

This sets up typing properly when the functions those files export are used in other parts of your code. I initially didn't do this and then did it but half wrong and typing was weird. But this exact setup works flawlessly.
Also the reason I added an optional key argument is so I can use the clients with specific keys, like the Service secret key for server-side tasks where a specific user's keys wont work. For example getting global statistics for the homepage, for both logged in and logged out users.

I also use it to do a task with specific users with cron/scheduled code through trigger.dev.

Obviously, just don't use a secret key on the client.ts's createClient, because then the secret will be on the client side, only use it with serverClient.
@Northern snakehead Hey, I got this working! I now imported supabase from client.ts and used it like this: typescript "use client"; import * as React from "react"; import { Session } from "@supabase/supabase-js"; import { createClient } from "@/utils/supabase/client"; // First a session context has to be created to listen to the auth changes, ref - https://supabase.com/docs/reference/javascript/auth-onauthstatechange (use React context for the user session). const SessionContext = React.createContext<Session | null>(null); export const SessionProvider = ({ children, }: { children: React.ReactNode; }) => { const [session, setSession] = React.useState<Session | null>(null); React.useEffect(() => { const supabase = createClient(); const fetchSession = async () => { const { data } = await supabase.auth.getSession(); setSession(data.session); }; fetchSession(); // Call the function const { data: authListener } = supabase.auth.onAuthStateChange((_event, session) => { setSession(session); }); return () => { authListener?.subscription?.unsubscribe(); }; }, []); return ( <SessionContext.Provider value={session}> {children} </SessionContext.Provider> ); }; export const useSessionContext = () => { const context = React.useContext(SessionContext); return context; }; Now, it's working fine 👍 Thanks
Chilean jack mackerel
Also this is the AuthProvider setup I use, which is likely similar to your end goal so I thought I would share. (Just note in this I manually make a type definition for Profile, which is a table in the DB, instead import it from /utils/types.ts made earlier)
The difference here is I can do const { user } = useAuth() which merges the normal user from supabase/onAuthStateChange, with the custom Profile table row for that user. E.g. if Profile has a username and avatar_url field, I can then do user.username etc
It also updates the Profile data along with the user any time the auth session updates from onAuthStateChange.
@Chilean jack mackerel Just a tip btw, you should initialize your client on client.ts like this: ts import { createBrowserClient } from "@supabase/ssr" import { SupabaseClient } from "@supabase/supabase-js" import { Database } from "./types" export default function createClient(key?: string): SupabaseClient<Database> { return createBrowserClient<Database>( process.env.NEXT_PUBLIC_SUPABASE_URL!, (key || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)! ) } And server.ts: ts import { cookies } from "next/headers" import { createServerClient } from "@supabase/ssr" import { SupabaseClient } from "@supabase/supabase-js" import { Database } from "./types" export default async function createClient(key?: string): Promise<SupabaseClient<Database>> { const cookieStore = await cookies() const isServiceClient = key === process.env.SUPABASE_SERVICE_KEY return createServerClient<Database>( process.env.NEXT_PUBLIC_SUPABASE_URL!, (key || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)!, { cookies: { getAll() { if (isServiceClient) return [] return cookieStore.getAll() }, setAll(cookiesToSet) { if (isServiceClient) return try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ) } catch { // The `setAll` method was called from a Server Component. // This can be ignored if you have middleware refreshing // user sessions. } }, }, } ) } With types.ts being generated with: `npx supabase gen types --lang=typescript --project-id YOUR_PROJECT_ID_HERE > utils/supabase/types.ts` This sets up typing properly when the functions those files export are used in other parts of your code. I initially didn't do this and then did it but half wrong and typing was weird. But this exact setup works flawlessly.
Northern snakeheadOP
Hi, thank you so much for the reference. I've updated my server and client according to this, it's working perfectly fine. Currently, working on the AuthProvider setup which you just sent.
Thanks 🙂