Firebase auth and React Hook - returning function from hook not working

Viewed 217

I have a custom auth hook that is mean to provide the user and loading state, as well as some user based actions (such as signing out). Here is my code:

type UserActions = {
  signOut(): Promise<void>;
};

const useUser = (): [
  firebase.User | undefined | null,
  UserActions,
  boolean,
  firebase.auth.Error | undefined
] => {
  const auth = firebase.auth();
  const [firebaseUser, loading, error] = useAuthState(auth);

  return [firebaseUser, { signOut: firebase.auth().signOut }, loading, error];
};

export default useUser;

When I use the signOut() function in another file after invoking the hook, I get an error Unhandled Rejection (TypeError): Cannot read property 'then' of undefined.

However, when I simply change my hook to be like so:

type UserActions = {
  signOut(): Promise<void>;
};

const useUser = (): [
  firebase.User | undefined | null,
  UserActions,
  boolean,
  firebase.auth.Error | undefined
] => {
  const auth = firebase.auth();
  const [firebaseUser, loading, error] = useAuthState(auth);

  const signOut = () => {
    return firebase.auth().signOut();
  };
  return [firebaseUser, { signOut }, loading, error];
};

export default useUser;

That works perfectly. What is the difference between these two cases? Not sure if I'm missing something about how arrow functions work, or maybe something with typing?

Thanks!

1 Answers

It's because you've changed the context of this which is needed by the implementation of signOut() (comments removed for conciseness):

fireauth.Auth.prototype.signOut = function() {
  var self = this;
  var p = this.redirectStateIsReady_.then(function() {
    if (self.authEventManager_) {
      self.authEventManager_.clearRedirectResult();
    }
    
    /* ... */
  }
  
  return this.registerPendingPromise_(p);
}

In your first attempt, you use:

{ signOut: firebase.auth().signOut }

which effectively creates:

{
  signOut() {
    var self = this;
    var p = this.redirectStateIsReady_.then(function() {
      if (self.authEventManager_) {
        self.authEventManager_.clearRedirectResult();
      }
    
      /* ... */
    }
  
    return this.registerPendingPromise_(p);
  }
}

In that code block, this (and self) are no longer an instance of the FirebaseAuth class, but are your object with a shape of { signOut: () => Promise<void> }. Which throws an error when you call signOut() because properties like redirectStateIsReady_ and authEventManager_ do not exist on that object (and they will be undefined).

This error:

Unhandled Rejection (TypeError): Cannot read property 'then' of undefined

is thrown because yourObject.redirectStateIsReady_ is undefined, which means that the code tries (and fails) to call (undefined).then().

In your second (simplified) attempt, you use:

const signOut = () => firebase.auth().signOut();

In this case, the signOut method hasn't been detached from it's FirebaseAuth class and functions as intended. This is known as "binding" a function and before arrow functions made it easier, this was done using:

const obj = firebase.auth();
const signOut = obj.signOut.bind(obj);
Related