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

feat(auth): auth-ready Promise #264

Closed
nicolasgarnier opened this Issue Sep 1, 2017 · 9 comments

Comments

Projects
None yet
2 participants
@nicolasgarnier

nicolasgarnier commented Sep 1, 2017

In some case I'd like to delay some events until I'm sure the auth state is ready and that the redux store contains the auth state of the signed-in user.

Typically I'd like to delay generating the UI until I'm sure that the auth state is ready which happens after the auth().onAuthStateChanged listener has fired once. There are multiple reasons why:

On the client I'd want to avoid a potential "refresh" of the UI where we'd display the UI of a signed-out user first and then redisplay once the redux store has loaded the signed-in users..

But this is really mandatory on the server as I really have to wait for the auth state to be ready because I only get one chance of generating the UI :)

In my app, as a workaround, I have a small tool that does this:

  // Auth state promise resolver.
  let authReadyPromiseResolver;
  const authReadyPromise = new Promise(resolve => {
    authReadyPromiseResolver = resolve
  });

  const unsubscribe = firebaseApp.auth().onAuthStateChanged(() => {
    authReadyPromiseResolver();
    unsubscribe();
  });

Then I can wait for the authReadyPromise promise to resolve and I know the firebase auth state si now ready. e.g. on a server I'd do:

// ...

authReadyPromise.then(() => {  // HERE I wait for the auth state to be ready.
      // Render the App.
      const body = ReactDOMServer.renderToString(
        React.createElement(app.App, {store: store, history: history})
      );
      // Get the redux store state. 
      const initialState = store.getState();
      // Serve the app generated template
      res.send(template({body, initialState}));
);

But really this is a workaround and I'm lucky that this works because my onAuthStateChanged event listener could very well be triggered before the one you use internally inside of react-redux-firebase.

So Feature request is:

It would be great to have a Promise I could use that would be built-in react-redux-firebase that would resolve when the auth state is ready and all the Redux store state has been updated. For instance, I'm thinking of:

const reactReduxFirebaseMiddleware = reactReduxFirebase(firebaseApp, {enableRedirectHandling: false});

const authIsReadypromise = reactReduxFirebaseMiddleware.authIsReady; // THIS IS THE BUILT-IN PROMISE

const store = createStore(
    combineReducers({
      ...reducers,
      router: routerReducer,
      firebaseState: firebaseStateReducer
    }),
    initialState,
    compose(
      applyMiddleware(thunk.withExtraArgument(getFirebase)),
      applyMiddleware(historyMiddleware),
      reactReduxFirebaseMiddleware
    )
  );

authIsReadypromise.then(() => {  // HERE I wait for the auth state to be ready.
      // Render the App.
      const body = ReactDOMServer.renderToString(
        React.createElement(app.App, {store: store, history: history})
      );

      // ...

Not sure if it should be on reactReduxFirebaseMiddleware or if there is somewhere more appropriate.

If there is another built in way to easily know when the auth state is ready please lmk, I'm kinda new-ish to redux so I may be missing some stuff :)

@prescottprue prescottprue added this to the v2.0.0 milestone Sep 2, 2017

@prescottprue

This comment has been minimized.

Show comment
Hide comment
@prescottprue

prescottprue Sep 2, 2017

Owner

Love the idea for the feature, and it is great to see how you think it would be used. I am going to start experimenting.

Here are a few existing things that may help with the meantime solution:

  • onAuthStateChange config option (hooks into internal onAuthStateChange)
  • AUTHENTICATION_INIT_FINISHED action type is emitted after auth is setup (maybe subscribe and wait for that?)
  • isLoaded and isEmpty helpers will be helpful if you are trying to check auth state existence
Owner

prescottprue commented Sep 2, 2017

Love the idea for the feature, and it is great to see how you think it would be used. I am going to start experimenting.

Here are a few existing things that may help with the meantime solution:

  • onAuthStateChange config option (hooks into internal onAuthStateChange)
  • AUTHENTICATION_INIT_FINISHED action type is emitted after auth is setup (maybe subscribe and wait for that?)
  • isLoaded and isEmpty helpers will be helpful if you are trying to check auth state existence

@prescottprue prescottprue changed the title from How to detect that Firebase auth is ready? (aka feature request for an auth-ready Promise)? to feat(auth): auth-ready Promise Sep 2, 2017

@nicolasgarnier

This comment has been minimized.

Show comment
Hide comment
@nicolasgarnier

nicolasgarnier Sep 18, 2017

Thanks @prescottprue !

FYI this is what I've used in the mean time:

/**
 * Returns a promise that completes when Firebase Auth is ready in the given store using react-redux-firebase.
 *
 * @param {Object} store - The Redux store on which we want to detect if Firebase auth is ready.
 * @param {string} [firebaseReducerAttributeName] - The attribute name of the react-redux-firebase reducer when using multiple combined reducers.
 *    'firebaseState' by default. Set this to `null` to indicate that the react-redux-firebase reducer is not in a combined reducer.
 * @return {Promise} - A promise that completes when Firebase auth is ready in the store.
 */
export function whenAuthReady(store, firebaseReducerAttributeName = 'firebaseState') {
  const isAuthReady = store => {
    const state = store.getState();
    const firebaseState = firebaseReducerAttributeName ? state[firebaseReducerAttributeName] : state;
    const firebaseAuthState = firebaseState && firebaseState.auth;
    if (!firebaseAuthState) {
      throw new Error(`The Firebase auth state could not be found in the store under the attribute '${firebaseReducerAttributeName ? firebaseReducerAttributeName + '.' : ''}auth'. Make sure your react-redux-firebase reducer is correctly set in the store`);
    }
    return firebaseState.auth.isLoaded;
  };

  return new Promise(accept => {
    if (isAuthReady(store)) {
      console.log('Redux store Firebase auth state is ready!');
      accept();
    } else {
      const unsubscribe = store.subscribe(() => {
        if (isAuthReady(store)) {
          console.log('Redux store Firebase auth state is ready!');
          unsubscribe();
          accept();
        }
      });
    }
  });
}

nicolasgarnier commented Sep 18, 2017

Thanks @prescottprue !

FYI this is what I've used in the mean time:

/**
 * Returns a promise that completes when Firebase Auth is ready in the given store using react-redux-firebase.
 *
 * @param {Object} store - The Redux store on which we want to detect if Firebase auth is ready.
 * @param {string} [firebaseReducerAttributeName] - The attribute name of the react-redux-firebase reducer when using multiple combined reducers.
 *    'firebaseState' by default. Set this to `null` to indicate that the react-redux-firebase reducer is not in a combined reducer.
 * @return {Promise} - A promise that completes when Firebase auth is ready in the store.
 */
export function whenAuthReady(store, firebaseReducerAttributeName = 'firebaseState') {
  const isAuthReady = store => {
    const state = store.getState();
    const firebaseState = firebaseReducerAttributeName ? state[firebaseReducerAttributeName] : state;
    const firebaseAuthState = firebaseState && firebaseState.auth;
    if (!firebaseAuthState) {
      throw new Error(`The Firebase auth state could not be found in the store under the attribute '${firebaseReducerAttributeName ? firebaseReducerAttributeName + '.' : ''}auth'. Make sure your react-redux-firebase reducer is correctly set in the store`);
    }
    return firebaseState.auth.isLoaded;
  };

  return new Promise(accept => {
    if (isAuthReady(store)) {
      console.log('Redux store Firebase auth state is ready!');
      accept();
    } else {
      const unsubscribe = store.subscribe(() => {
        if (isAuthReady(store)) {
          console.log('Redux store Firebase auth state is ready!');
          unsubscribe();
          accept();
        }
      });
    }
  });
}

prescottprue added a commit that referenced this issue Sep 28, 2017

store.firebaseAuthIsLoaded added - #264
* firebaseStateName constant - assumed name of firebase state to be
used in authIsLoaded
* attachAuthIsLoaded constant - boolean for enabling/disabling the
attaching of firebaesAuthIsLoaded to store (true by default)
* authIsLoaded is exported so it can be used directly
@prescottprue

This comment has been minimized.

Show comment
Hide comment
@prescottprue

prescottprue Sep 28, 2017

Owner

Got something added to v2.0.0-beta.9 (not published yet though). It actually uses basically exactly what you provided @nicolasgarnier.

Since reactReduxFirebase is a store enhancer it can/does attach things to the store (for not just store.firebase). For this case I attached a promise called firebaseAuthIsReady to the store.

firebaseAuthIsReady

const store = createStore(
    combineReducers({
      ...reducers,
      router: routerReducer,
      firebaseState: firebaseStateReducer
    }),
    initialState,
    compose(
      reactReduxFirebase(firebaseApp, {
         firebaseStateName: 'firebaseState', // is 'firebase' by default, so you must set to 'firebaseState'
         enableRedirectHandling: false
      }),
      applyMiddleware(thunk.withExtraArgument(getFirebase)), // Only needed if using getFirebase within thunks
      applyMiddleware(historyMiddleware),
    )
  );

store.firebaseAuthIsReady.then(() => { // state is ready here
      // Render the App.
})

Enable/Disable Attachment

attachAuthIsLoaded config option was added to allow for disabling the attaching of thefirebaseAuthIsLoaded promise to the redux store.

reactReduxFirebase(firebaseApp, {
  attachAuthIsReady: false // store.firebaseAuthIsReady will be undefined
})

Custom authIsReady logic

For those who want to change how the internal creation of the authIsReady promise can provide a function to the authIsReady config parameter.

reactReduxFirebase(firebaseApp, {
  attachAuthIsReady: true, // true by default
  authIsReady: (store, config) => {
    // write your own logic here, but make sure to return a promise!
  }
})

By Itself

The internal authIsReady function is also exposed so it can be used by itself. It accepts store and firebaseStateName

import { authIsReady } from 'react-redux-firebase'

// authIsReady(store, firebaseStateName)
authIsReady(store, 'firebase')
  .then(() => {
    console.log('auth is ready')
  })
Owner

prescottprue commented Sep 28, 2017

Got something added to v2.0.0-beta.9 (not published yet though). It actually uses basically exactly what you provided @nicolasgarnier.

Since reactReduxFirebase is a store enhancer it can/does attach things to the store (for not just store.firebase). For this case I attached a promise called firebaseAuthIsReady to the store.

firebaseAuthIsReady

const store = createStore(
    combineReducers({
      ...reducers,
      router: routerReducer,
      firebaseState: firebaseStateReducer
    }),
    initialState,
    compose(
      reactReduxFirebase(firebaseApp, {
         firebaseStateName: 'firebaseState', // is 'firebase' by default, so you must set to 'firebaseState'
         enableRedirectHandling: false
      }),
      applyMiddleware(thunk.withExtraArgument(getFirebase)), // Only needed if using getFirebase within thunks
      applyMiddleware(historyMiddleware),
    )
  );

store.firebaseAuthIsReady.then(() => { // state is ready here
      // Render the App.
})

Enable/Disable Attachment

attachAuthIsLoaded config option was added to allow for disabling the attaching of thefirebaseAuthIsLoaded promise to the redux store.

reactReduxFirebase(firebaseApp, {
  attachAuthIsReady: false // store.firebaseAuthIsReady will be undefined
})

Custom authIsReady logic

For those who want to change how the internal creation of the authIsReady promise can provide a function to the authIsReady config parameter.

reactReduxFirebase(firebaseApp, {
  attachAuthIsReady: true, // true by default
  authIsReady: (store, config) => {
    // write your own logic here, but make sure to return a promise!
  }
})

By Itself

The internal authIsReady function is also exposed so it can be used by itself. It accepts store and firebaseStateName

import { authIsReady } from 'react-redux-firebase'

// authIsReady(store, firebaseStateName)
authIsReady(store, 'firebase')
  .then(() => {
    console.log('auth is ready')
  })

@prescottprue prescottprue referenced this issue Sep 29, 2017

Merged

v2.0.0 beta.9 #281

3 of 3 tasks complete

prescottprue added a commit that referenced this issue Oct 11, 2017

v2.0.0 beta.9 (#281)
* `reloadAuth` added for reloading auth (calls `firebase.auth().currentUser.reload()`) - #273
* `linkWithCredential` added for linking auth with credential - #268 
* `store.firebaseAuthIsReady` is now added by `reactReduxFirebase` store enhancer - promise that resolves once auth state is ready - #264 
* `authIsReady` promise added for waiting for auth to be ready - #264
* `ordered` always set as `null` instead of `undefined` - fixes possible issue of `isLoaded` not always being correct
* `firebaseStateName` constant - assumed name of firebase state to be used in `authIsReady`
* `attachAuthIsLoaded` constant - boolean for enabling/disabling the
attaching of `firebaseAuthIsReady` to store (`true` by default)
* `yarn.lock` removed - npm5 is faster
* `v2.0.0` branch added to travis config (so v2.0.0 pushes/merges are built)
* `babel-preset-env` used in place of `babel-preset-es2015` (fixes deprecation warning)
@prescottprue

This comment has been minimized.

Show comment
Hide comment
@prescottprue

prescottprue Oct 12, 2017

Owner

Included in the v2.0.0-beta.9 release. Let me know if it doesn't work as expected or cover what you needed. Thanks for the feature suggestion!

Owner

prescottprue commented Oct 12, 2017

Included in the v2.0.0-beta.9 release. Let me know if it doesn't work as expected or cover what you needed. Thanks for the feature suggestion!

@nicolasgarnier

This comment has been minimized.

Show comment
Hide comment
@nicolasgarnier

nicolasgarnier Nov 20, 2017

Hey Scott,

I'm trying to use this feature now but I am not able to.

Here is my code to create the store:

export function makeStore(history, firebaseApp, initialState = {}) {
  const historyMiddleware = routerMiddleware(history);
  const firebaseEnhancer = reactReduxFirebase(firebaseApp, {enableRedirectHandling: false});
  const store = createStore(
    combineReducers({
      ...reducers,
      router: routerReducer,
      firebaseState: firebaseStateReducer
    }),
    initialState,
    compose(
      applyMiddleware(thunk.withExtraArgument(getFirebase)),
      applyMiddleware(historyMiddleware),
      firebaseEnhancer
    )
  );
  store.firebaseAuthIsReady().then(() => console.log('AUTH READY')); // This FAILS
  firebaseEnhancer.authIsReady().then(() => console.log('AUTH READY')); // This FAILS TOO
  return store;
}

I'm always getting errors such as There was an error TypeError: store.firebaseAuthIsReady is not a function.

And yes I made sure I upgraded to beta.9 and above :)

Any idea?

nicolasgarnier commented Nov 20, 2017

Hey Scott,

I'm trying to use this feature now but I am not able to.

Here is my code to create the store:

export function makeStore(history, firebaseApp, initialState = {}) {
  const historyMiddleware = routerMiddleware(history);
  const firebaseEnhancer = reactReduxFirebase(firebaseApp, {enableRedirectHandling: false});
  const store = createStore(
    combineReducers({
      ...reducers,
      router: routerReducer,
      firebaseState: firebaseStateReducer
    }),
    initialState,
    compose(
      applyMiddleware(thunk.withExtraArgument(getFirebase)),
      applyMiddleware(historyMiddleware),
      firebaseEnhancer
    )
  );
  store.firebaseAuthIsReady().then(() => console.log('AUTH READY')); // This FAILS
  firebaseEnhancer.authIsReady().then(() => console.log('AUTH READY')); // This FAILS TOO
  return store;
}

I'm always getting errors such as There was an error TypeError: store.firebaseAuthIsReady is not a function.

And yes I made sure I upgraded to beta.9 and above :)

Any idea?

@nicolasgarnier

This comment has been minimized.

Show comment
Hide comment
@nicolasgarnier

nicolasgarnier Nov 20, 2017

I just also tried:

  firebaseEnhancer.authIsReady.then(() => console.log('AUTH READY'));
  store.firebaseAuthIsReady.then(() => console.log('AUTH READY'));

Since firebaseAuthIsReady is probably directly a Promise and not a function but I get this error:

info: There was an error TypeError: Cannot read property 'then' of undefined

nicolasgarnier commented Nov 20, 2017

I just also tried:

  firebaseEnhancer.authIsReady.then(() => console.log('AUTH READY'));
  store.firebaseAuthIsReady.then(() => console.log('AUTH READY'));

Since firebaseAuthIsReady is probably directly a Promise and not a function but I get this error:

info: There was an error TypeError: Cannot read property 'then' of undefined

@nicolasgarnier

This comment has been minimized.

Show comment
Hide comment
@nicolasgarnier

nicolasgarnier Nov 20, 2017

OK I found the issue. It looks like the config variable to enable this is not set to true by default.

Also we need to use attachAuthIsReady: true and not attachAuthIsLoaded as indicated in the beta.9 release notes :) probably this was changed after beta.9

nicolasgarnier commented Nov 20, 2017

OK I found the issue. It looks like the config variable to enable this is not set to true by default.

Also we need to use attachAuthIsReady: true and not attachAuthIsLoaded as indicated in the beta.9 release notes :) probably this was changed after beta.9

@nicolasgarnier

This comment has been minimized.

Show comment
Hide comment
@nicolasgarnier

nicolasgarnier Nov 20, 2017

@prescottprue

I'm facing an issue where firebase.auth.isLoaded is never true initially if we are starting with no signed-on user. My expectation would be that firebase.auth.isLoaded is true whenever the initial auth state is reflected in the Redux store, no matter if there is a signed-in user or not.

Basically if I use this new feature after beta.9 to wait to load the entire UI the UI of my app is never loaded (because every user eventually start the app without any Firebase user signed-in. And basically the Promise never resolves.

It looks like this is a regression in beta.9 I don't think I was facing this issue in beta 8.

nicolasgarnier commented Nov 20, 2017

@prescottprue

I'm facing an issue where firebase.auth.isLoaded is never true initially if we are starting with no signed-on user. My expectation would be that firebase.auth.isLoaded is true whenever the initial auth state is reflected in the Redux store, no matter if there is a signed-in user or not.

Basically if I use this new feature after beta.9 to wait to load the entire UI the UI of my app is never loaded (because every user eventually start the app without any Firebase user signed-in. And basically the Promise never resolves.

It looks like this is a regression in beta.9 I don't think I was facing this issue in beta 8.

@nicolasgarnier

This comment has been minimized.

Show comment
Hide comment

nicolasgarnier commented Nov 20, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment