简体   繁体   中英

Nexts.js 13 + Supabase > What's the proper way to create a user context

I'm building an app with Next.js 13 and Supabase for the backend, and I've been stuck on figuring out the best/proper way to go about creating a context/provider for the current logged in user.

The flow to retrieve the user from Supabase is this:

  1. Sign in with an OAuth Provider.
  2. Grab the user ID from the session from the supabase onAuthState Changed hook.
  3. Fetch the full user object from the supabase DB with the user ID mentioned above.

I have a supabase listener in my layout that listens for the auth state changes, and works well for setting and refreshing current session. My initial approach was to add the fetchUser call from within the onAuthState changed hook, however I was running into late update hydration errors.

Taken directly from the examples, this is how the app looks:

// layout.tsx
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const supabase = createServerComponentSupabaseClient<Database>({
    headers,
    cookies,
  });
  const {
    data: { session },
  } = await supabase.auth.getSession();

  return (
    <html>
      <head />
      <body>
        <NavMenu session={session} />
        <SupabaseListener accessToken={session?.access_token} />
        {children}
      </body>
    </html>
  );
}
// supabase-listener.tsx
// taken directly from the supabase-auth-helpers library.

"use client";

import { useRouter } from "next/navigation";
import { useEffect } from "react";
import supabase from "../lib/supabase/supabase-browser";

export default function SupabaseListener({
  accessToken,
}: {
  accessToken?: string;
}) {
  const router = useRouter();

  useEffect(() => {
    supabase.auth.onAuthStateChange(async (event, session) => {
      if (session?.access_token !== accessToken) {
        router.refresh();
      }
    });
  }, [accessToken, router]);

  return null;
}

I basically just need to wrap my root layout with a LoggedInUserProvider, make the fetch user call somewhere in the initial page load, and set the state of the current logged in user provider.

The other approaches I tried was making the fetch user call from the root layout, and having a LoggedInUserListener client component that takes the user as a property and simply sets the state if the profile exists. This was causing improper set state errors.

Thank you so much.

Check out this PR for a better example of how to structure the application and add a provider for sharing a single instance of Supabase client-side, as well as the session from the server

If you follow a similar pattern, then your additional query for the full user record should go immediately after you get the session in examples/nextjs-server-components/app/layout.tsx . You could then pass this as a prop to the <SupabaseProvider /> and share it across the application from context's value prop.

I am following your awesome auth-helpers example but my context from the provider keeps coming back as null for user details. Is there anything wrong with the code below or is there some isLoading logic that will work better for getting that data?

Also want to confirm, does the SupabaseProvider in the root layout pass down to all other child layout components?

   'use client';

import type { Session } from '@supabase/auth-helpers-nextjs';
import { createContext, useContext, useState, useEffect } from 'react';
import type { TypedSupabaseClient } from 'app/layout';
import { createBrowserClient } from 'utils/supabase-client';
import { UserDetails, CompanyDetails } from 'models/types';

type MaybeSession = Session | null;

type SupabaseContext = {
  supabase: TypedSupabaseClient;
  session: MaybeSession;
  userDetails: UserDetails | null;
  isLoading: boolean;
};

// @ts-ignore
const Context = createContext<SupabaseContext>();

//TODO get stripe subscription data
export default function SupabaseProvider({
  children,
  session
}: {
  children: React.ReactNode;
  session: MaybeSession;
}) {
  const [supabase] = useState(() => createBrowserClient());
  const [userDetails, setUserDetails] = useState<UserDetails | null>(null);
  const [isLoading, setLoading] = useState(false);
  // Hydrate user context and company data for a user
  useEffect(() => {
    const fetchUserDetails = async () => {
      if (session && session.user) {
        setLoading(true);
        const { data } = await supabase
          .from('users')
          .select('*, organizations (*)')
          .eq('id', session.user.id)
          .single();
        //TODO fix types
        setUserDetails(data as any);
        setLoading(false);
      }
    };
    if (session) {
      fetchUserDetails();
    }
  }, [session, supabase]);
  return (
    <Context.Provider value={{ supabase, session, userDetails, isLoading }}>
      <>{children}</>
    </Context.Provider>
  );
}

export const useSupabase = () => useContext(Context);

 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM