How to update client component after server action was triggered?

I have a Next.js app and I’m using supabase for authentification.

In my navbar (which is a client component) I have a login button which should change to a welcome message after a successful login. I need this to be a client component so that it can still work with SSG.

The login happens as a server action which means once it triggers, the user is logged in but the client component doesn’t know about it.

Is there any clean way to trigger the onAuthStateChange to pick up the login state change?

Hi @valentinilas, thanks for reaching out in the community.

I found the following code on Use Supabase Auth with React | Supabase Docs and I think this should work as a client component. Can you try this logic?

  import './index.css'
  import { useState, useEffect } from 'react'
  import { createClient } from '@supabase/supabase-js'
  import { Auth } from '@supabase/auth-ui-react'
  import { ThemeSupa } from '@supabase/auth-ui-shared'

  const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>')

  export default function WelcomeComponent() {
    const [session, setSession] = useState(null)

    useEffect(() => {
      supabase.auth.getSession().then(({ data: { session } }) => {
        setSession(session)
      })

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

      return () => subscription.unsubscribe()
    }, [])

    if (!session) {
      return (null)
    }
    else {
      return (<div>Welcome message!</div>)
    }
  }

The problem is if a server action triggers the session change, the client component won’t know about it as they are completely separated. I’m wondering how can I let the client component know that the user or session has been altered.
Initially I tried to pass it as a prop to the client component but that breaks SSG

Hi @valentinilas, oh I see. I thought the onAuthStateChange event is triggered anyways.

If you need more help, please share your public repo or a minimal reproducible example. That will let us all work together from the same code to figure out what’s going wrong.

@anshumanb, certainly!

Here’s what I tried:

User logs in → Server action is triggered → Returns success message → Trigger a refresh (here it breaks)

The data seems fine in all server components, they instantly get updated but the header client component doesn’t

Client side header component:

export default function HeaderAuth() {
    const [user, setUser] = useState<User | null>(null);
    const [loading, setLoading] = useState(true);
    const router = useRouter()
    const supabase = createClient(); // client side

    useEffect(() => {

        const checkSession = async () => {
            const { data: { session } } = await supabase.auth.getSession();
            console.log("Initial session check:", session); // Debug log
            setUser(session?.user ?? null);
            setLoading(false);
        };

        const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
            console.log("Auth state changed:", event, session); // Debug log
            setUser(session?.user ?? null);
            setLoading(false);
        });

        checkSession();

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

    const handleLogout = async () => {
        const result = await logout()
        if (result.success) {
            setUser(null)
            router.push("/")
            router.refresh()
        } else {
            console.error("Logout failed:", result.error)
        }
    }

    return (
        <div>
            {user ? (
                <div>
                    <span>Hello, {user.email}!</span>
                    <button onClick={handleLogout}>Logout</button>
                </div>
            ) : (
                <Link href="/login">
                    <LogInIcon className="mr-2" /> Log in
                </Link>
            )}
        </div>
    )
}

Login action (server side)

export async function login(prevState: { error: string | null; success: boolean }, formData: FormData) {
  const supabase = await createClient()

  // type-casting here for convenience
  // in practice, you should validate your inputs
  const data = {
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  }

  const { error } = await supabase.auth.signInWithPassword(data)

  if (error) {
    return { error: error.message, success: false }
  }
  revalidatePath('/', 'layout')
  return { error: null, success: true }

}

Login page (client side)

"use client"
import { useActionState, useEffect } from 'react'
import { login, signup } from './actions'
import { useRouter } from 'next/navigation'
export default function LoginPage() {

  const [state, formAction, isPending] = useActionState(login, { error: null, success: false })
  const router = useRouter()

  useEffect(() => {
    if (state?.success) {
      router.push("/private")
      router.refresh();
    }
  }, [state, router])

  return (
    <form action={formAction}>
      <label htmlFor="email">Email:</label>
      <input id="email" name="email" type="email" required />
      <label htmlFor="password">Password:</label>
      <input id="password" name="password" type="password" required />
      <button>Log in</button>
      {/* <button formAction={signup}>Sign up</button> */}
      {state?.error && <p className="error">{state.error}</p>}
      {isPending && <p>Please wait...</p>}

    </form>
  )
}

Hi @valentinilas, thanks for sharing the code. Have you tried using the redirect function in the login server action to redirect the user to /private directly on success?

Yes, Here is a simplified example:

Server action:

export async function login(formData: FormData) {
  const supabase = await createClient()
  const data = {
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  }
  const { error } = await supabase.auth.signInWithPassword(data)
  if (error) {
    redirect('/error')
  }
  revalidatePath('/', 'layout')
  redirect('/private')
}

Login page

"use client"
import { login} from './actions'
export default function LoginPage() {
  return (
    <form >
      <label htmlFor="email">Email:</label>
      <input id="email" name="email" type="email" required />
      <label htmlFor="password">Password:</label>
      <input id="password" name="password" type="password" required />
      <button formAction={login}>Log in</button>
    </form>
  )
}

What happens is when the user logs in → the redirect kicks in and loads the /private route where I can see the correct user data inside the server component, however this does not seem to refresh the Header component which is still unaware of the user being logged in.
If i hard refresh the page by pressing F5, or use alt-tab on the browser window, then it’s working.

1 Like

I was able to fix it by creating a context around the app for the user.
And then changing the login server action to return the user:

export async function login(formData: FormData) {
  const supabase = await createClient()
  const data = {
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  }
  const { data: authData, error } = await supabase.auth.signInWithPassword(data)
  if (error) {
    throw new Error(error.message)
  }
  revalidatePath('/')
  return authData.session?.user || null
}

And then on the login page which is client side, update the user:

"use client"
import { login } from "./actions"
import { useRouter } from "next/navigation"
import { useUser } from "@/context/UserContext"

export default function LoginPage() {
  const router = useRouter()
  const { setUser } = useUser()

  async function handleSubmit(formData: FormData) {
    const loggedInUser = await login(formData) // Modify login to return user
    if (loggedInUser) {
      setUser(loggedInUser)
      router.refresh()      // Force the header to re-check session
      router.push("/private")
    } else {
      // Handle login error if needed
    }
  }

  return (
    <form onSubmit={async (e) => {
      e.preventDefault()
      await handleSubmit(new FormData(e.currentTarget))
    }}>
      <label htmlFor="email">Email:</label>
      <input id="email" name="email" type="email" required />
      <label htmlFor="password">Password:</label>
      <input id="password" name="password" type="password" required />
      <button type="submit">Log in</button>
    </form>
  )
}
2 Likes

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.