Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

session() and user() reportedly not returning data on first invocation #23

Closed
awalias opened this issue Nov 6, 2020 · 17 comments · Fixed by #30
Closed

session() and user() reportedly not returning data on first invocation #23

awalias opened this issue Nov 6, 2020 · 17 comments · Fixed by #30
Labels
bug Something isn't working released

Comments

@awalias
Copy link
Member

awalias commented Nov 6, 2020

image

@awalias awalias added the bug Something isn't working label Nov 6, 2020
@awalias
Copy link
Member Author

awalias commented Nov 6, 2020

@thorwebdev or @kiwicopple might have ideas here

@thorwebdev
Copy link
Member

Both client.session() and client.user() are synchronous methods that only return a value when there is an active session, otherwise they are expected to return null.

Do you have any more context as to why they are expecting a session to be existent?

@SweeToxin
Copy link

Thank you @awalias and Hi @thorwebdev.
To put some context onto this.
I have a function that fires on app load to find if there's a user or a session and set my app storage to the loggedin user.

1: signIn is not called initially since this goes on app load.
2: This is used for 2 cases ( as of now ) the first is to set the user after a external auth/reset password redirect, the second one is on app reload/refresh to verify if there's a session as to not ask the user to login after each page reload.

the behavior noticed is that when called once ( or only one of them ) it still returns null while the localstorage is already filled with the session info.

Hope this was clear enough, if you require more info I would be glad to answer.

@thorwebdev
Copy link
Member

@SweeToxin I'm not able to reproduce this, can you provide a CodeSandbox that shows this behaviour?

There are two scenarios here:

1. Check on page load whether a user is logged in:

This works synchronously as expected. When GoTrue is initialized (which happens when the supabase client is created) the session is recovered synchronously from localstorage (https://github.com/supabase/gotrue-js/blob/master/src/GoTrueClient.ts#L57) and supabase.auth.user() returns the user object synchronously on first call.

2. Login from link (confirm signup / recover password / magic link)

This is an asynchronous action as GoTrue needs to get the access_token from the URL and then retrieve the session from the server (https://github.com/supabase/gotrue-js/blob/master/src/GoTrueClient.ts#L187-L209).

So in this scenario, supabase.auth.user() will initially return null until the session & user data has been loaded. This is where you want to use the supabase.auth.onAuthStateChange listener.

So in a React application, you will want to do something like this:

useEffect(() => {
    setSession(supabase.auth.session()); // will return the user object for scenario 1 // null for scenario 2
    setUser(supabase.auth.user()); // will return the user object for scenario 1 // null for scenario 2

    // for scenario 2, this will fire a SIGNED_IN event shortly after page load once the session has been loaded from the server.
    const { data: authListener } = supabase.auth.onAuthStateChange(
      (event, session) => {
        console.log(`Supbase auth event: ${event}`); 
        setSession(session);
        setUser(session?.user ?? null);
      }
    );
    return () => {
      authListener.unsubscribe();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

You can find a next.js auth example here: https://github.com/supabase/supabase/tree/master/examples/nextjs-with-supabase-auth also running on CodeSandbox: https://codesandbox.io/s/github/thorwebdev/nextjs-with-supabase-auth?file=/utils/useUser.js

Hope this helps.

@SweeToxin
Copy link

SweeToxin commented Nov 7, 2020

Hello, @thorwebdev,

This sandbox should reproduce the bug.

https://codesandbox.io/s/icy-star-u37g0?file=/App.svelte

start it, login and reload the page, this will reproduce scenario 1.

@awalias did an update on the instance I'm using and I think it solved the redirect issue, the realtime seems to be reacting to it correctly.

Thank you again for your time.

@thorwebdev
Copy link
Member

@SweeToxin thanks. I'm able to reproduce with the Svelte example. The crazy thing is, if you introduce even just a delay of 1ms it works fine: https://codesandbox.io/s/dreamy-haze-2k8rl?file=/App.svelte

Does this mean svelte renders too quickly? Or is this potentially related to server-side rendering, maybe some hydration issues?

@kiwicopple any ideas what we can do to fix this?

@sw-yx I know you've done some stuff with Svelte. Do you maybe have an idea?

@calendee
Copy link
Contributor

calendee commented Dec 9, 2020

I discovered I also needed to add a timeout in my React Native projects.

setTimeout(() => {
    setSupabase(client)
}, 100)

@calendee
Copy link
Contributor

calendee commented Dec 9, 2020

The issue is that when you call the supabase client constructor:

const supabase = createClient(...)

It does the following:

    this.currentUser = null
    this.currentSession = null

Because createClient is synchronous, when you immediately follow with:

supabase.auth.user() or supabase.auth.session(), you're going to get the null from the constructor.

However, in the constructor, the asynchronous method this._recoverSession() is called to recover any session info from storage.

Unfortunately, by the time you've called any of the supabase.auth.?? methods, the session has not yet been recovered. That's why introducing any delay that is faster than fetching from async storage solves the problem.

Perhaps if we made the constructor return a promise?

constructor(options: {
  url?: string
  headers?: { [key: string]: string }
  detectSessionInUrl?: boolean
  autoRefreshToken?: boolean
  persistSession?: boolean
  localStorage?: Storage
}) {
  const settings = { ...DEFAULT_OPTIONS, ...options }
  this.currentUser = null
  this.currentSession = null
  this.autoRefreshToken = settings.autoRefreshToken
  this.persistSession = settings.persistSession
  this.localStorage = new LocalStorage(settings.localStorage)
  this.api = new GoTrueApi({ url: settings.url, headers: settings.headers })

  return (async() {
    await this._recoverSession()
    // Handle the OAuth redirect
    try {
      if (settings.detectSessionInUrl && isBrowser() && !!getParameterByName('access_token')) {
        this.getSessionFromUrl({ storeSession: true })
      }
    } catch (error) {
      console.log('Error getting session from URL.')
    }
  })();
}

Then, we could do:

const supabase = await createClient(...)

@thorwebdev
Copy link
Member

In a normal web scenario _recoverSession should be synchronous as the web api localstorage methods are synchronous: https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem

However, I guess in a RN scenario you're using AsyncStorage which might be the issue?

However, for createClient to return a Promise would be a pretty breaking change at this point. I'd rather have us fire an onAuthStateChange SIGNED_IN event (https://supabase.io/docs/client/auth-onauthstatechange) instead. Would that work for you?

@github-actions
Copy link
Contributor

🎉 This issue has been resolved in version 1.9.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@thorwebdev
Copy link
Member

Okay, the following pattern should solve things across the board: https://github.com/thorwebdev/nextjs-subscription-payments/blob/main/components/UserContext.js#L13-L28

useEffect(() => {
    const session = supabase.auth.session();
    setSession(session);
    setUser(session?.user ?? null);
    const { data: authListener } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        setSession(session);
        setUser(session?.user ?? null);
      }
    );

    return () => {
      authListener.unsubscribe();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

For svelte, see an example here: https://codesandbox.io/s/optimistic-newton-4whlg?file=/App.svelte

@calendee
Copy link
Contributor

Nice! Thanks for the suggestion @thorwebdev!

@chipilov
Copy link

Okay, the following pattern should solve things across the board: https://github.com/thorwebdev/nextjs-subscription-payments/blob/main/components/UserContext.js#L13-L28

useEffect(() => {
    const session = supabase.auth.session();
    setSession(session);
    setUser(session?.user ?? null);
    const { data: authListener } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        setSession(session);
        setUser(session?.user ?? null);
      }
    );

    return () => {
      authListener.unsubscribe();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

For svelte, see an example here: https://codesandbox.io/s/optimistic-newton-4whlg?file=/App.svelte

@thorwebdev Feel free to correct me if I am missing something, but I don't think this solves all issues. I've described in separate open ticket what the problem is: #143 (comment)

If I am correct, I am not sure if we should re-open this one or just continue the work on #143 - hence, I post this here, as well.

mustefa added a commit to s-a-n-d-a-g-e-n-c-y/l3ms that referenced this issue Mar 6, 2022
@dotcomstruggle
Copy link

dotcomstruggle commented Aug 2, 2022

As of August 2, 2022; I'm experiencing a similar issue on React Native.

Due to this problem, the login screen flashes for a split second before the app redirects to an internal screen based on the current sessions that's retrieved using supabase.auth.session() on load.

To remedy the issue, I tried all the recommended solutions in this thread, no luck though. In addition I tried to create a custom useSupabase() hook in which I called createClient() in a setTimeout() with 1000ms delay, but issue persists.

In any case, thanks for all the community members & maintainers for taking their valuable time to come up with solutions. TBH this is one of the best OS communities out there.

FWIW, here's my AuthContext which controls all things auth in my project. If you have any suggestions as to how/why my login screen is flashing I would love to hear & try it.

Edit

After fiddling with the code a little more I came up with a solution based on the information provided by @calendee. Instead of wrapping createClient with a setTimout (which didn't work in my case), I wrapped the initial session setter functions found in my useEffect with a setTimeout. I provide the before & after of the changed code below. If you want to see the whole file, the GitHub link is above this edit.

Before 👇

useEffect(() => {
  const session = supabase.auth.session();
  setSession(session);
  setUser(session?.user ?? null);
  setIsLoadingInitial(false);

  const { data: authListener } = supabase.auth.onAuthStateChange(
    async (_event, session) => {
      setSession(session);
      setUser(session?.user ?? null);
    }
  );

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

After 👇

useEffect(() => {
  setTimeout(() => {
    const session = supabase.auth.session();
    setSession(session);
    setUser(session?.user ?? null);
    setIsLoadingInitial(false);
  }, 100);

  const { data: authListener } = supabase.auth.onAuthStateChange(
    async (_event, session) => {
      setSession(session);
      setUser(session?.user ?? null);
    }
  );

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

@jdgamble555
Copy link

For supabase-js v2 and rxjs, I did this:

  authState(): Observable<User | null> {
    return new Observable((subscriber: Subscriber<User | null>) => {
      this.supabase.auth.getSession().then(session =>
        subscriber.next(session.data.session?.user)
      );
      const auth = this.supabase.auth.onAuthStateChange(async ({ }, session) => {
        subscriber.next(session?.user);
      });
      return auth.data.subscription.unsubscribe;
    });
  }

This really should be the default option for supabase-js, so that the original value is part of the observable.

J

@Eirikalv1
Copy link

Eirikalv1 commented Jul 8, 2023

As far as i can understand i am facing the same issue in react native. The first time the session is printed to the console, it is always null. That makes the initial route in Stack.Navigator navigate to the wrong page. I tried using timeouts as suggested above, but did not seem to work.

const [session, setSession] = useState(null);

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

    supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session)
    })
  }, [])
  console.log(session);

I grabbed that from the getting started with expo guide: https://supabase.com/docs/guides/getting-started/tutorials/with-expo

Thanks for any help :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working released
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants