Skip to content

Instantly share code, notes, and snippets.

@dyaa
Last active February 10, 2024 08:58
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dyaa/8f8d1f8964160630f2475fe26a2e6150 to your computer and use it in GitHub Desktop.
Save dyaa/8f8d1f8964160630f2475fe26a2e6150 to your computer and use it in GitHub Desktop.
Lazy Load Import Firebase (dynamic import)
Promise.all([
import('firebase/app'),
import('firebase/database'),
import('firebase/auth'),
])
.then(x => x[0].default)
.then(firebase => {
const config = {
apiKey: '',
authDomain: '',
databaseURL: '',
projectId: '',
storageBucket: '',
messagingSenderId: '',
}
firebase.initializeApp(config)
if (global.firebase) {
return global.firebase
} else if (!firebase) {
return Promise.reject(new Error('loading error'))
} else {
global.firebase = firebase
const googleAuthProvider = new firebase.auth.GoogleAuthProvider()
googleAuthProvider.addScope('https://www.googleapis.com/auth/userinfo.email')
global.firebase.googleAuthProvider = googleAuthProvider
return global.firebase ? global.firebase : firebase
}
})
.catch(err => {
throw new Error(err)
})
@jperasmus
Copy link

Nice one! One thing I think might bite you is the imports running in parallel. AFAIK, the firebase/app always needs to be imported first and then the other services will mutate/patch the firebase object when they're imported and made available.

@dyaa
Copy link
Author

dyaa commented Dec 18, 2019

@jperasmus Thanks for your comment, By that you mean importing/resolving firebase/app then import the extras, am i right?

@hypernova7
Copy link

hypernova7 commented Mar 15, 2020

And how would it be using async/await?

@jperasmus
Copy link

With async/await it would look like this:

const initFirebase = async () => {
  const firebase = await import("firebase/app");

  await Promise.all([import("firebase/database"), import("firebase/auth")]);

  const config = {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: ""
  };

  firebase.initializeApp(config);

  if (global.firebase) {
    return global.firebase;
  }

  if (!firebase) {
    return Promise.reject(new Error("loading error"));
  }

  global.firebase = firebase;

  const googleAuthProvider = new firebase.auth.GoogleAuthProvider();
  googleAuthProvider.addScope("https://www.googleapis.com/auth/userinfo.email");

  global.firebase.googleAuthProvider = googleAuthProvider;

  return global.firebase ? global.firebase : firebase;
};

initFirebase()
  .then(firebase => // Do what you want with firebase app instance)
  .catch(console.error);

@hypernova7
Copy link

Thanks @jprasmus :)

@rodbs
Copy link

rodbs commented Sep 20, 2020

Hi,

I don't understand the global variable you're using. What is that? Is it for avoiding duplicating the initialization?
I understand this is Node. Do you know how it could be implemented in NextJs?
thx

@jperasmus
Copy link

Hi @rodbs

I personally do this to import the firebase client and admin SDK's as I need them in Next.js

The client SDK

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
}

export const getFirebaseClient = async () => {
  const firebase = await import('firebase/app')

  await Promise.all([
    import('firebase/auth'),
    import('firebase/storage'),
    import('firebase/firestore'),
  ])

  if (!firebase.apps.length) {
    firebase.initializeApp(firebaseConfig)
  }

  return firebase
}

The admin SDK

import * as admin from 'firebase-admin'

export const getAdminClient = () => {
  if (!admin.apps.length) {
    const privateKeyBuffer = Buffer.from(
      process.env.FIREBASE_PRIVATE_KEY_BASE64 || '',
      'base64'
    )
    const firebasePrivateKey = privateKeyBuffer.toString('utf8')

    admin.initializeApp({
      credential: admin.credential.cert({
        projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
        clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
        privateKey: firebasePrivateKey,
      }),
      databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
      storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
    })
  }

  return admin
}

@rodbs
Copy link

rodbs commented Sep 21, 2020

Thanks a lot @jperasmus

I'm having problems consuming the async firebase. T
-When I try to call the async firabse I'm getting this error: Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
-Should I return resolvef promises in the useProvideAuth for then to use in the context and around the rest of the modules?

This is my former auth.js module. How would you consume the async firebase? thanks!

const authContext = createContext();

export function AuthProvider({ children }) {
  const auth = useProvideAuth();
  return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}

export const useAuth = () => {
  return useContext(authContext);
};

function useProvideAuth() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  
  const handleUser = async (rawUser) => {
    if (rawUser) {
     

      const user = await formatUser(rawUser);

      //dont want to save the token in the db
      const { token, ...userWithoutToken } = user;

      await createUser(user.uid, userWithoutToken);
       

      setUser(user);
      console.log(user);

      //to send auth users to my page (defined at the Head in index.js)
      // cookie.set('igls-auth', true), { expires: 1 };
      return user;
    } else {
      setUser(false);
      // cookie.remove('igls-auth');
      return false;
    }
  };

  
  const signinWithGoogle = (redirect) => {
    setLoading(true);
    return firebase
      .auth()
      .signInWithPopup(new firebase.auth.GoogleAuthProvider())
      .then((response) => {
        handleUser(response.user);

        if (redirect) {
          Router.push(redirect);
        }
      });
  };
 

  useEffect(() => {
    const unsubscribe = firebase.auth().onAuthStateChanged(handleUser);

    return () => unsubscribe();
  }, []);

  return {
    user,     
    signinWithGoogle,
    
  };
}
 
const formatUser = async (user) => {
  );
  return {
    uid: user.uid,
    email: user.email,
    name: user.displayName,
    token: user.xa,
    
  };
};```

@jperasmus
Copy link

I'm not 100% sure about your use case, but based on the error you are getting, it sounds like you are calling on of your custom hooks somewhere outside of a React function component. For class components or other utils, you can use those helpers that I posted previously to work directly with Firebase. You can always wrap those helpers in a custom hook as well to use in your React function components, but is not necessary.

@rodbs
Copy link

rodbs commented Sep 21, 2020

@jperasmus I've created this hook, but I cannot get it to work. It's not returning anything. I've tried with the isLoading and w/o it, and it doesn't work. Is it for you the right way to resolve the promise and get the firebase instance? Thanks

export const useFirebase = () => {
  const [firebase, setFirebase] = useState();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const loadFirebase = async () => {
      setIsLoading(true);
      try {
        const fire = await getFirebaseClient();
         setFirebase(fire);
        setIsLoading(false); 
      } catch (error) {
        setError(error);
        setIsLoading(false);
      }
    };
    loadFirebase();   
  }, []);

  return { firebase, isLoading, error };
}```

@jperasmus
Copy link

This hook looks fine to me. You can optimize it by using a reducer instead of 3 different state variables because the hook will run at least 4 times (once for each "setState"), but that is besides the point here. Where are you running this hook from? Is it running inside a function component?

One other thing I'm thinking of now is that if the component you are calling this from looks something like this:

const YourComponent = () => {
  const { firebase, isLoading, error } = useFirebase()

  if (error) {
    return <ErrorComponent />
  }

  if (isLoading) {
    return <Loader />
  }

  return (
    <MainComponent firebase={firebase} />
  )
}

The first render, isLoading will be false and error will be null, so it will skip the checks, but firebase will still be undefined. Maybe that is happening now? You can default your isLoading state to true instead.

@rodbs
Copy link

rodbs commented Sep 22, 2020

Thanks, eventually I've made it work.
But even it's working now in lazy mode I've noticed that in the browser the full firebase library is still loaded in the initial chunk. Shouldn't it be the opposite? to load it on demand?
(I'm using 'next/dynamic' that I think it's the equivalent to React.Lazy)

@jperasmus
Copy link

I'm not sure. I think that depends on the Webpack config Next.js is using under the hood, but generally, if you use the import() syntax a separate chunk will be created. Only thing I can think of right now is to make sure you aren't importing or including Firebase somewhere else already.

@rodbs
Copy link

rodbs commented Sep 22, 2020

ok, I guess I need to dig into it a bit. Thanks!

@husayt
Copy link

husayt commented Dec 26, 2020

I added caching and here is what I ended up with:

async function getFirebaseClient(){
  const { default: firebase } = await import("firebase/app")

  await Promise.all([
    import("firebase/auth"),
    //   import('firebase/storage'),
    import("firebase/firestore"),
  ])

  const config = {
.....
  }

  firebase.initializeApp(config)
  firebase.firestore()
  return firebase
}

let cached = null

export function fb() {

    if (cached || process.server) return cached

    cached = getFirebaseClient()
    return cached
}

This is how it can be used

import { fb } from "~/js/firebase-loader"


// get lazy loading started
fb()


async function sample(){
   const firebase = await fb()
    firebase.auth()....
}

One benefit is that fb can be imported from multiple places, but still the result will be shared.

@mafergus
Copy link

Very nice @husayt

@mimiqkz
Copy link

mimiqkz commented Jan 30, 2021

I have error when trying these steps with typescripts

Hi @rodbs

I personally do this to import the firebase client and admin SDK's as I need them in Next.js

The client SDK

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
}

export const getFirebaseClient = async () => {
  const firebase = await import('firebase/app')

  await Promise.all([
    import('firebase/auth'),
    import('firebase/storage'),
    import('firebase/firestore'),
  ])

  if (!firebase.apps.length) {
    firebase.initializeApp(firebaseConfig)
  }

  return firebase
}

The admin SDK

import * as admin from 'firebase-admin'

export const getAdminClient = () => {
  if (!admin.apps.length) {
    const privateKeyBuffer = Buffer.from(
      process.env.FIREBASE_PRIVATE_KEY_BASE64 || '',
      'base64'
    )
    const firebasePrivateKey = privateKeyBuffer.toString('utf8')

    admin.initializeApp({
      credential: admin.credential.cert({
        projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
        clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
        privateKey: firebasePrivateKey,
      }),
      databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
      storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
    })
  }

  return admin
}

but adding const firebase = (await import('firebase/app')).default into it seems to work

@rodbs
Copy link

rodbs commented Jan 30, 2021

Thanks @mimiqkz

@qxygene
Copy link

qxygene commented Feb 2, 2022

any update for v9 ?

@Jaseibert
Copy link

@qxygene Yeah, I stopped on this thread during a 6-hour journey to find how to lazy load the treeshakeable imports for Firebase v9. Everyone seemed nice, so I came back with a solution. Shout out to https://twitter.com/gabe_ragland/status/1417516444686839809 for the og solution. Hope this helps anyone struggling on v9 like I was.

Firebase Setup

/***** firebase/app.js *****/

import fbConfig from "./config";
import { initializeApp, getApp, getApps } from "firebase/app";

const setupFirebase = () => {
  if (getApps.length) return getApp();
  return initializeApp( /* Your Firebase Config */);
};

export const app = setupFirebase();



/***** firebase/auth.js *****/

import { getAuth } from "firebase/auth";
import { app } from "./app.js";

// Export initialized Firestore "auth"
export const auth = getAuth(app);

//Export just what you need
export {
  signInWithEmailAndPassword,
  createUserWithEmailAndPassword,
} from "firebase/auth";



/***** firebase/firestore.js *****/

import { getFirestore } from "firebase/firestore";
import { app } from "./app.js";

// Export initialized Firestore "DB"
export const db = getFirestore(app);

//Export just what you need
export {
  collection,
  query,
  where,
  orderBy,
  onSnapshot,
} from "firebase/firestore";

Usage in Project

// Dynamically import subset of Auth Methods
const getAuth = () => import("../firebase/auth.js")

// Dynamically import subset of Firestore Methods
const getDb = () => import("../firebase/firestore.js")

/**
* Get them both at the same time
* Usage: const { firestore: { db, query, collection, where, orderBy, onSnapshot } } = await getFirebaseAll()
*/
const getFirebaseAll = () => {
  return Promise.all([
    import("../firebase/auth"),
    import("../firebase/firestore"),
  ]).then(([auth, firestore]) => {
    return { auth, firestore };
  });
};

/****** Example ******/

const onSignup = async (email, password) => {
    // Use await to ensure the library is loaded 
    const { auth, createUserWithEmailAndPassword } = await getFirebase()

    // Call the method like normal
    const { user } = await createUserWithEmailAndPassword(auth, email, password)

    // Do whatever else
} 

credit: https://twitter.com/gabe_ragland/status/1417516444686839809

@qxygene
Copy link

qxygene commented Feb 17, 2022

thank you, works nice. But one thing for auth; when i refresh page and update doc, i receive an error => FirebaseError: Missing or insufficient permissions. If i dont refresh page all works fine.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
 match /users/{document=**} {
      allow read;
      allow write: if request.auth != null;
    }
  }
}

@augustmuir
Copy link

augustmuir commented Apr 6, 2022

@Jaseibert Thanks this works great.

To add on, I also made my own firebase types/interface files (I am using typescript), then I re-wrote firebase code I needed to use which I couldn't wait for (such as Timestamp) in my own files without the bloat. Lastly I made sure I absolutely never imported from firebase/(any of them) outside of the root firestore files were created from your guide.

The second step of the optimization I explained above took and extra 125kB+ off the bundle. After optimizing just firebase my first load JS shared by all went from 320kB to 125kB! (I use functions, storage, firestore, and auth)

You can use this eslint rule to prevent importing firebase directly:
"no-restricted-imports": ["error", "firebase/app", "firebase/firestore", "firebase/storage", "firebase/auth", "firebase/functions"]

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