How to import Firebase only on client in Sapper? - firebase

I'm importing Firebase into my Sapper application, I do not want the imports to be evaluated on the server. How do I make sure imports are only on the client-side?
I am using Sapper to run sapper export which generates the static files. I have tried:
Creating the firebase instance in it's own file and exported the firebase.auth() and firebase.firestore() modules.
Trying to adjust the rollup.config.js to resolve the dependencies differently, as suggested from the error message below. This brings more headaches.
Creating the Firebase instance in client.js. Unsuccessful.
Creating the instance in stores.js. Unsuccessful.
Declaring the variable and assigning it in onMount(). This causes me to have to work in different block scopes. And feels a bit hacky.
The initialization of the app, works fine:
import firebase from 'firebase/app'
const config = {...}
firebase.initializeApp(config);
I have also discovered that if I change the import to just import firebase from 'firebase' I do not get this server error:
#firebase/app:
Warning: This is a browser-targeted Firebase bundle but it appears it is being run in a Node environment. If running in a Node environment, make sure you are using the bundle specified by the "main" field in package.json.
If you are using Webpack, you can specify "main" as the first item in
"resolve.mainFields": https://webpack.js.org/configuration/resolve/#resolvemainfields
If using Rollup, use the rollup-plugin-node-resolve plugin and set "module" to false and "main" to true: https://github.com/rollup/rollup-plugin-node-resolve
I expected to just export these firebase functionalities from a file and import them into my components like:
<script>
import { auth } from "../firebase";
</script>
But as soon as that import is include, the dev server crashes. I don't want to use it on the server, since I'm just generating the static files.
Does anyone have some ideas on how to achieve importing only on client side?

So I have spent too much time on this. There isn't really a more elegant solution than onMOunt.
However, I did realize that sapper really should be used for it's SSR capabilities. And I wrote an article about how to get set up on Firebase with Sapper SSR and Cloud Functions:
https://dev.to/eckhardtd/how-to-host-a-sapper-js-ssr-app-on-firebase-hmb
Another solution to original question is to put the Firebase CDN's in the global scope via the src/template.html file.
<body>
<!-- The application will be rendered inside this element,
because `app/client.js` references it -->
<div id='sapper'>%sapper.html%</div>
<!-- Sapper creates a <script> tag containing `app/client.js`
and anything else it needs to hydrate the app and
initialise the router -->
%sapper.scripts%
<!-- Insert these scripts at the bottom of the HTML, but before you use any Firebase services -->
<!-- Firebase App (the core Firebase SDK) is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/6.0.4/firebase-app.js"></script>
<!-- Add Firebase products that you want to use -->
<script src="https://www.gstatic.com/firebasejs/6.0.4/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/6.0.4/firebase-firestore.js"></script>
</body>
</html>
and in the component:
<script>
import { onMount } from 'svelte';
let database, authentication;
onMount(() => {
database = firebase.firestore();
authentication = firebase.auth();
});
const authHandler = () => {
if (process.browser) {
authentication
.createUserWithEmailAndPassword()
.catch(e => console.error(e));
}
}
</script>
<button on:click={authHandler}>Sign up</button>

I was able to import firebase using ES6. If you are using rollup you need to consfigure namedExports in commonjs plugin:
//--- rollup.config.js ---
...
commonjs({
namedExports: {
// left-hand side can be an absolute path, a path
// relative to the current directory, or the name
// of a module in node_modules
'node_modules/idb/build/idb.js': ['openDb'],
'node_modules/firebase/dist/index.cjs.js': ['initializeApp', 'firestore'],
},
}),
The you can use it like this:
//--- db.js ---
import * as firebase from 'firebase';
import 'firebase/database';
import { firebaseConfig } from '../config'; //<-- Firebase initialization config json
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
export { firebase };
// Initialize db
export const db = firebase.firestore();
and maybe use it in a service like such:
// --- userService.js ----
import { db } from './common';
const usersCol = db.collection('users');
export default {
async login(username, password) {
const userDoc = await usersCol.doc(username).get();
const user = userDoc.data();
if (user && user.password === password) {
return user;
}
return null;
},
};
EDITED
Full rollup config
/* eslint-disable global-require */
import resolve from 'rollup-plugin-node-resolve';
import replace from 'rollup-plugin-replace';
import commonjs from 'rollup-plugin-commonjs';
import svelte from 'rollup-plugin-svelte';
import babel from 'rollup-plugin-babel';
import { terser } from 'rollup-plugin-terser';
import config from 'sapper/config/rollup';
import { sass } from 'svelte-preprocess-sass';
import pkg from './package.json';
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
const legacy = !!process.env.SAPPER_LEGACY_BUILD;
// eslint-disable-next-line no-shadow
const onwarn = (warning, onwarn) =>
(warning.code === 'CIRCULAR_DEPENDENCY' && warning.message.includes('/#sapper/')) || onwarn(warning);
export default {
client: {
input: config.client.input(),
output: config.client.output(),
plugins: [
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode),
}),
svelte({
dev,
hydratable: true,
emitCss: true,
preprocess: {
style: sass(),
},
}),
resolve({
browser: true,
}),
commonjs({
namedExports: {
// left-hand side can be an absolute path, a path
// relative to the current directory, or the name
// of a module in node_modules
'node_modules/idb/build/idb.js': ['openDb'],
'node_modules/firebase/dist/index.cjs.js': ['initializeApp', 'firestore'],
},
}),
legacy &&
babel({
extensions: ['.js', '.mjs', '.html', '.svelte'],
runtimeHelpers: true,
exclude: ['node_modules/#babel/**'],
presets: [
[
'#babel/preset-env',
{
targets: '> 0.25%, not dead',
},
],
],
plugins: [
'#babel/plugin-syntax-dynamic-import',
[
'#babel/plugin-transform-runtime',
{
useESModules: true,
},
],
],
}),
!dev &&
terser({
module: true,
}),
],
onwarn,
},
server: {
input: config.server.input(),
output: config.server.output(),
plugins: [
replace({
'process.browser': false,
'process.env.NODE_ENV': JSON.stringify(mode),
}),
svelte({
generate: 'ssr',
dev,
}),
resolve(),
commonjs(),
],
external: Object.keys(pkg.dependencies).concat(require('module').builtinModules || Object.keys(process.binding('natives'))),
onwarn,
},
serviceworker: {
input: config.serviceworker.input(),
output: config.serviceworker.output(),
plugins: [
resolve(),
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode),
}),
commonjs(),
!dev && terser(),
],
onwarn,
},
};

The clean way is to use the Dynamic Import as the documentation said: Making a component SSR compatible
The way to get around this is to use a dynamic import for your component, from within the onMount function (which is only called on the client), so that your import code is never called on the server.
So here for example we want to import the core of firebase and the authentication package too.
<script>
let firebase;
onMount(async () => {
const module = await import("firebase/app");
await import("firebase/auth");
firebase = module.default;
firebase.initializeApp(firebaseConfig);
});
<script>
And now you can use firebase object as you can, for example we want to login with email and password:
let email;
let password;
async function login() {
try {
let result = await firebase.auth().signInWithEmailAndPassword(
email,
password
);
console.log(result.user);
} catch (error) {
console.log(error.code, error.message);
}
}

In order to use Firebase with Sapper, you have to import firebase not firebase/app. You do want firebase to be able to load correctly with SSR on the backend, not just the frontend. If you have some metatags, for example, that would be stored in the database, you want them to load on the backend (UNTESTED).
You could just use firebase, but then you get the annoying console warning. Remember also firebase loads ALL firebase dependencies while firebase/app does not, that is why you don't want to use it on the frontend. There is probably a way with admin-firebase, but we want to have less dependencies.
Do not use rxfire at all. You don't need it. It causes errors with Sapper. Just plain Firebase.
firebase.ts
import firebase from 'firebase/app';
import "firebase/auth";
import "firebase/firestore";
import * as config from "./config.json";
const fb = (process as any).browser ? firebase : require('firebase');
fb.initializeApp(config);
export const auth = fb.auth();
export const googleProvider = new fb.auth.GoogleAuthProvider();
export const db = fb.firestore();
Firebase functions require an extra step and you must enable dynamic imports. (UNTESTED)
export const functions = (process as any).browser ? async () => {
await import("firebase/functions");
return fb.functions()
} : fb.functions();
While this compiles, I have not tried to run httpsCallable or confirmed it will load from the database on the backend for seo ssr from the db. Let me know if it works.
I suspect all of this will work with the new SvelteKit now that Sapper is dead.

Related

how can I use 'vue3-google-oauth' with boot files in Quasar?

I am trying to implement Google OAuth2 using 'vue3-google-oauth'.
I tried the method below, but the signIn() function returned false.
Please tell me how to use 'vue3-google-oauth' with boot files in Quasar.
this is my code.
googleoauth.js
import { boot } from 'quasar/wrappers';
import GAuth from 'vue3-google-oauth2';
let app;
export default boot(({ app }) => {
const gAuthOption = {
clientId: '835170725866-nt6qibip4tb7jafav2k20j83ipso67mo.apps.googleusercontent.com',
scope: 'profile email',
prompt: 'select_account',
};
app.use(GAuth, gAuthOption);
app = app;
});
export { app };
quasar.config.js
...
boot: ['googleoauth'],
...
LoginPage.vue ----- I want to use Google OAuth2 in this file
import { getCurrentInstance } from 'vue';
const app = getCurrentInstance();
app.appContext.config.globalProperties.$gAuth.signIn(); // return false (not working)

Using module bundlers and dynamic config firebase

I have an issue where I cannot use init.js, which I have done in the past when importing firebase through the reserved hosting urls.
<script src="/__/firebase/init.js"></script>
This is the script that I am trying to use, and I am importing my firebase modules with:
import * as firebase from "firebase/app";
But I am trying to use webpack, with this. I have tried including init.js in my html, and my bundle as well, but without success.
Is there any way to use init.js with my module bundler?
Pretty late to the party, but better late than never if somebody needs it :)
We wanted to do the same at my company, for two reasons
not having to specify the firebase config ourself, as the init.js script contains it
automatic pickup of emulators when the useEmulator=true query param is specified (handy when you want to choose which emulators to start)
So we trick the script into doing what we want!
Basically the script looks for a global firebase variable which does not exist when using the modules. So we provide one for it to work.
Here's the code we use:
import { initializeApp } from 'firebase/app';
import { connectAuthEmulator, getAuth } from 'firebase/auth';
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
import { connectFunctionsEmulator, getFunctions } from 'firebase/functions';
export async function initFirebase(): Promise<void> {
// provide a fake firebase proxy object to the hosting init script
// to get the boilerplate done (firebase conf + automatic connection
// to started emulators)
const proxy: any = {
initializeApp: (c: any) => initializeApp(c),
};
// emulators done under development check to help with the tree-shaking
if (process.env.NODE_ENV === 'development') {
proxy.firestore = () => ({
useEmulator: (h: any, p: any) =>
connectFirestoreEmulator(getFirestore(), h, p),
});
proxy.functions = () => ({
useEmulator: (h: any, p: any) =>
connectFunctionsEmulator(getFunctions(), h, p),
});
proxy.auth = () => ({
useEmulator: (h: any, p: any) => connectAuthEmulator(getAuth(), h, p),
});
}
const _global = globalThis as any;
_global.firebase = proxy;
try {
await import(`/__/firebase/init.js${
process.env.NODE_ENV === 'development' ? '?useEmulator=true' : ''
}`
);
} finally {
delete _global.firebase;
}
}
You'll notice I only setup the proxies for firestore/functions/auth as those are the only ones we emulate at the moment. If you don't care for the emulators, you can skip the entire code paths when process.env.NODE is 'development'.

React Redux Firebase: Error on firebaseConnect - Cannot read property 'ordered' of undefined

I followed the example in the documentation under v2.0.0 > Read Me > Load Data (listeners automatically managed on mount/unmount) (direct link is not possible).
And replaced the connect call with the firestore specific one shown here](http://react-redux-firebase.com/docs/firestore.html#examples) in Example 1.
I copied the Todo example exactly in a new component created for testing purposes.
Todo Component:
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { compose } from 'redux'
import { firebaseConnect,firestoreConnect, isLoaded, isEmpty } from 'react-redux-firebase'
const Todos = ({ todos, firebase }) => {
// Build Todos list if todos exist and are loaded
const todosList = !isLoaded(todos)
? 'Loading'
: isEmpty(todos)
? 'Todo list is empty'
: Object.keys(todos).map(
(key, id) => (
<TodoItem key={key} id={id} todo={todos[key]}/>
)
)
return (
<div>
<h1>Todos</h1>
<ul>
{todosList}
</ul>
<input type="text" ref="newTodo" />
<button onClick={this.handleAdd}>
Add
</button>
</div>
)
}
// export default compose(
// firestoreConnect([
// 'todos' // { path: '/todos' } // object notation
// ]),
// connect((state) => ({
// todos: state.firestore.data.todos,
// profile: state.firestore.profile // load profile
// }))
// )(Todos)
export default compose(
firestoreConnect(['todos']), // or { collection: 'todos' }
connect((state, props) => ({
todos: state.firestore.ordered.todos
}))
)(Todos)
The store configuration was configured as shown here in the docs. The store configuration was adapted to slot into the framework created by react-boilerplate.
/**
* Create the store with dynamic reducers
*/
import { createStore, applyMiddleware, compose } from 'redux'
import { fromJS } from 'immutable'
import { routerMiddleware } from 'connected-react-router/immutable'
import createSagaMiddleware from 'redux-saga'
import { reactReduxFirebase, firebaseReducer } from 'react-redux-firebase'
import { reduxFirestore, firestoreReducer } from 'redux-firestore'
import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/database'
import 'firebase/firestore'
import createReducer from './reducers'
const sagaMiddleware = createSagaMiddleware()
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.AUTH_DOMAIN,
databaseURL: process.env.DATABASE_URL,
projectId: process.env.PROJECT_ID,
storageBucket: process.env.STORAGE_BUCKET,
messagingSenderId: process.env.MESSAGING_SENDER_ID,
}
const rrfConfig = {
userProfile: 'users',
// useFirestoreForProfile: true, // Firestore for Profile instead of Realtime DB
// attachAuthIsReady: true
}
// Initialize Cloud Firestore through Firebase
export default function configureStore(initialState = {}, history) {
firebase.initializeApp(firebaseConfig)
// Initialize Firestore with timeshot settings
firebase.firestore()
// firebase.firestore().settings({ timestampsInSnapshots: true })
// Create the store with two middlewares
// 1. sagaMiddleware: Makes redux-sagas work
// 2. routerMiddleware: Syncs the location/URL path to the state
const middlewares = [sagaMiddleware, routerMiddleware(history)]
const enhancers = [
applyMiddleware(...middlewares),
// reactReduxFirebase(config), // enhancing our store with these packages
// reduxFirestore(config)
]
// If Redux DevTools Extension is installed use it, otherwise use Redux compose
/* eslint-disable no-underscore-dangle, indent */
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose
/* eslint-enable */
const createStoreWithFirebase = compose(
reactReduxFirebase(firebase, rrfConfig), // firebase instance as first argument
reduxFirestore(firebase),
)(createStore)
const store = createStoreWithFirebase(
createReducer(),
fromJS(initialState),
composeEnhancers(...enhancers),
)
// Extensions
store.runSaga = sagaMiddleware.run
store.injectedReducers = {} // Reducer registry
store.injectedSagas = {} // Saga registry
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
module.hot.accept('./reducers', () => {
store.replaceReducer(createReducer(store.injectedReducers))
})
}
return store
}
I traced and verified my store configuration exactly to make sure all steps present in the documentation are configured correctly in my configuration.
My createReducer funciton is in a seperate file and you can see that I added the firebaseReducer and firebaseReducer correctly.
import { combineReducers } from 'redux-immutable'
import { connectRouter } from 'connected-react-router/immutable'
import { firebaseReducer } from 'react-redux-firebase'
import { firestoreReducer } from 'redux-firestore'
import history from 'utils/history'
import languageProviderReducer from 'containers/LanguageProvider/reducer'
export default function createReducer(injectedReducers = {}) {
const rootReducer = combineReducers({
firebase: firebaseReducer,
firestore: firestoreReducer,
language: languageProviderReducer,
...injectedReducers,
})
// Wrap the root reducer and return a new root reducer with router state
const mergeWithRouterState = connectRouter(history)
return mergeWithRouterState(rootReducer)
}
My redux store contains the firestore and firebase and it is injected into the component props.
What does not work is the use of connectFirestore HoC to automatically retrieve and inject a list of documents in to the component.
This is the error message:
react-dom.development.js?61bb:20266 Uncaught TypeError: Cannot read property 'ordered' of undefined
at Function.eval [as mapToProps] (index.js?d834:49)
at mapToPropsProxy (wrapMapToProps.js?1817:54)
at Function.detectFactoryAndVerify (wrapMapToProps.js?1817:63)
at mapToPropsProxy (wrapMapToProps.js?1817:54)
at handleFirstCall (selectorFactory.js?805c:37)
at pureFinalPropsSelector (selectorFactory.js?805c:85)
at Object.runComponentSelector [as run] (connectAdvanced.js?48b8:43)
at Connect.initSelector (connectAdvanced.js?48b8:195)
at new Connect (connectAdvanced.js?48b8:136)
at constructClassInstance (react-dom.development.js?61bb:11315)
(Snipped from my code which is the example 1 in documentation):
export default compose(
firestoreConnect(['todos']), // or { collection: 'todos' }
connect((state, props) => ({
todos: state.firestore.ordered.todos
}))
)(Todos)
I inspected the state variable and it does contain the firestore attribute. This attribute contains a number of functions, as expected, but it is missing the query results under "ordered", which is undefined.
I have tried all different ways to use firestoreconnect e.g. using a Class-based component, using a query with parameters, etc. and all give the same error.
My Firebase project is configured correct as I am able to create documents inside collections. A todos collection for testing purposes is present as well containing 2 documents.
I have come across this post, which mentions the following:
If you just upgraded to React-Redux v6, it's because react-redux-firebase is not compatible with v6.
See https://github.com/prescottprue/react-redux-firebase/issues/581 for details.
This does not apply to me because I am using react-redux version 5. Here are the versions I am using:
"firebase": "^5.10.1",
"react-redux": "^5.0.7",
"react-redux-firebase": "^2.2.6",
"redux": "^4.0.1",
"redux-firestore": "^0.7.3",
I have spent a significant amount of time on this. Like I said, using firestore to add new data to collections works fine. It is just this HoC business that is failing no matter how i approach the solution.
any help would be appreciated.
Never solved this. I guess it is related to incompatible versions. What I ended up doing is download v4 of react-boilerplate and set up v3 react-redux-firebase which uses the Context API as opposed to store enhancers. Now works very well.

Nuxt + Firebase: Firebase App named '[DEFAULT]' already exists (app/duplicate-app)

I'm writing my own nuxt plugin for firebase/firestore. I'm loading it in the config with:
plugins: [
'~/plugins/firestore.js',
{ src: '~/plugins/vuex-persist', ssr: false }
],
The file itself looks like this
import Vue from 'vue'
import * as firebase from 'firebase/app';
import 'firebase/firestore';
const config = {
...
}
firebase.initializeApp(config)
const settings = {
timestampsInSnapshots: true
}
const store = firebase.firestore()
store.settings(settings)
class Firestore {
...
}
const firestore = new Firestore()
export default ({ app }, inject) => {
inject('firestore', firestore)
}
When I'm running npm run dev it tries to create a firebase instance on each automatic reload. How can I have it only create this instance once?
Firebase: Firebase App named '[DEFAULT]' already exists (app/duplicate-app).
You can check firebase.apps to see if its loaded. If your only loading it once, then you can just check the length. If you have multiple then you could check each apps name. Once it helped for me
if (!firebase.apps.length) {
firebase.initializeApp({});
}

Firebase initialization error in VueJS Project

first time posting here. Also quite new to VueJS, know some Angular.
I ran into an error when initializing Firebase(first time getting this error) in my VueJS project:
That showed up briefly after click event, so I had to slow down my network to take screenshot.
I've included <script src="https://www.gstatic.com/firebasejs/5.5.5/firebase.js"></script> in index.html
I also installed firebase(latest version) via npm with --save
And created 2 files: firebaseInit.js and firebaseConfig.js(which contains the api key, etc for web app):
In firebaseInit.js:
import firebase from 'firebase';
import 'firebase/firestore';
import firebaseConfig from './firebaseConfig';
const firebaseApp=
firebase.initializeApp(firebaseConfig);
export default firebaseApp.firestore();
and put:
export default { the config key value pairs obtained
from Firebase console }
in firebaseConfig.js.
Extra info(though I don't think this is the source of the problem):
I imported firebase with import firebase as 'firebase' in the vue file I used firebase auth() and createUserWithEmailandPassword() in the methods property, but alert() doesn't work.
import firebase from 'firebase';
export default {
name: 'register',
data: function() {
return {
email: '',
password: ''
};
},
methods: {
register: function(e) {
firebase
.auth()
.createUserWithEmailAndPassword(this.email, this.password)
.then(
user => {
alert(`Account made for ${user.email}`);
this.$router.push('/');
},
err => {
console.log(err.message);
}
);
e.preventDefault();
}
}
};
You created your firebaseInit.js but since you never import or require it, is it never executed, thus resulting in a "firebase not initialized" error when you try to use the firebase you imported from the generic firebase module instead.
Use export default firebaseApp in your firebaseInit.js (and not firestore as you do now) and then import firebase from 'path/to/firebaseInit.js' for example.

Resources