dijonmusters / build-a-saas-with-next-js-supabase-and-stripe

292 stars 67 forks source link

User Authentication State doesn't match on Server and Client when Refresh Page #6

Open myhendry opened 2 years ago

myhendry commented 2 years ago

The user state doesn't match on the server and the client

When I logged in and refresh a page, I see the following error in my browser console

next-dev.js?3515:32 Warning: Text content did not match. Server: "Auth" Client: "Exit"

In my browser, my user state returns a boolean true that shows I'm authenticated; however, in my server, my user state returns a boolean false that shows I'm not authenticated

If I navigate via the client side without refreshing the page, the auth status works fine

Anyone knows why is my server and client authentication status different? I followed the example in the youtube video. Anything I missed out? Thanks

Full Repo

AuthContext.tsx

interface AuthUser extends User {
  is_subscribed: boolean;
  interval: string;
}
export interface IAuthContext {
  // setUser: Dispatch<SetStateAction<any>>;
  user: AuthUser | null;
  loginWithMagicLink: (email: string) => Promise<{ error: any | null }>;
  signOut: () => Promise<{ error: ApiError | null } | undefined>;
  isLoading: boolean;
}

export const AuthContext = createContext<IAuthContext>(null!);

interface IProps {
  supabaseClient: SupabaseClient;
}

const AuthProvider: FC<IProps> = ({ children }) => {
  const [user, setUser] = useState<AuthUser | null>(
    supabase.auth.user() as AuthUser
  );
  const [isLoading, setIsLoading] = useState<boolean>(true);
  const { push } = useRouter();

  useEffect(() => {
    const getUserProfile = async () => {
      const sessionUser = supabase.auth.user();

      if (sessionUser) {
        const { data: profile } = await supabase
          .from("profile")
          .select("*")
          .eq("id", sessionUser.id)
          .single();

        setUser({
          ...sessionUser,
          ...profile,
        });

        setIsLoading(false);
      }
    };

    getUserProfile();

    supabase.auth.onAuthStateChange(() => {
      getUserProfile();
    });

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    try {
      axios.post(`/api/set-supabase-cookie`, {
        event: user ? "SIGNED_IN" : "SIGNED_OUT",
        session: supabase.auth.session(),
      });
    } catch (error) {
      console.log(error);
    }
  }, [user]);

  const loginWithMagicLink = async (email: string) => {
    const data = await supabase.auth.signIn({ email });
    return data;
  };

  const signOut = async () => {
    try {
      const data = await supabase.auth.signOut();
      setUser(null);
      push("/auth");
      return data;
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <AuthContext.Provider
      value={{
        loginWithMagicLink,
        user,
        signOut,
        isLoading,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error(`useUser must be used within a UserContextProvider.`);
  }
  return context;
};

export default AuthProvider;

Navbar.tsx

export const Navbar = ({ title = "L A B" }: Props) => {
  const { user, signOut } = useAuth();
  console.log("nav isAuthenticated", !!user);

  return (
    <div className="navbar bg-base-100 shadow-lg">
      <div className="flex-1">
        <Link href="/">
          <a className="btn btn-ghost normal-case text-xl">
            <span className="text-lg font-bold tracking-widest">{title}</span>
          </a>
        </Link>
      </div>

      <div className="flex-none">
        <ul className="menu menu-horizontal p-0">
          {links.map((l) => (
            <li key={l.title} className="hidden md:block">
              <Link href={l.url}>
                <a className="cursor-pointer">{l.title}</a>
              </Link>
            </li>
          ))}

          {!!user ? (
            <li className="hidden md:block">
              <a onClick={signOut}>Exit</a>
            </li>
          ) : (
            <li className="hidden md:block">
              <Link href="/auth">
                <a className="cursor-pointer">Auth</a>
              </Link>
            </li>
          )}
          <li tabIndex={0} className="md:hidden">
            <a>
              <svg
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                className="inline-block w-5 h-5 stroke-current"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                  d="M4 6h16M4 12h16M4 18h16"
                ></path>
              </svg>
            </a>

            <ul className="bg-base-100">
              {links.map((l) => (
                <li key={l.title}>
                  <Link href={l.url}>
                    <a
                      className="cursor-pointer tooltip tooltip-left"
                      data-tip={l.tip}
                    >
                      {l.icon}
                    </a>
                  </Link>
                </li>
              ))}
              {!!user ? (
                <li>
                  <a
                    onClick={signOut}
                    className="cursor-pointer tooltip tooltip-left"
                    data-tip="Exit"
                  >
                    <AiFillAliwangwang size={40} color="red" />
                  </a>
                </li>
              ) : (
                <li>
                  <Link href="/auth">
                    <a
                      className="cursor-pointer tooltip tooltip-left"
                      data-tip="Auth"
                    >
                      <AiFillAccountBook size={40} color="green" />
                    </a>
                  </Link>
                </li>
              )}
            </ul>
          </li>
        </ul>
        <ThemeChanger />
      </div>
    </div>
  );
};
vbuser2004 commented 2 years ago

I was experiencing the same issue during refresh and when completing or cancelling a payment.

I believe it is caused by the user being set to supabase.auth.user() in the user.js context file when setting the initial useState. I changed this to null and the error has been eliminated. I haven't seen any other negative issue or error since making this change.

`const Provider = ({ children }) => {

const [user, setUser] = useState(null); //<---- this changed to null

const [isLoading, setIsLoading] = useState(true);

const router = useRouter(); .... `

WeaverOfTheWeb commented 1 year ago

You could also just set the user state back to supabase.auth.user() on logout so it keeps it consistent and provide a null user attribute if the user is logged out.

const signOut = async () => {
    try {
      const data = await supabase.auth.signOut();
      setUser(supabase.auth.user()); // will result in { user: null }
      push("/auth");
      return data;
    } catch (error) {
      console.log(error);
    }
  };