Using Angular with Firebase has changing slightly since version 16. While we navigate closer to standalone components, the library is evolving. There are new ways to do the same old thing, and options depending on your needs. Here I make the same ol app, but in Angular: A Todo App.
TL;DR#
Here I show you how to setup Angular with AngularFire so that you can use Firebase with your Angular app. You can also see how to setup signals with the user for authentication, and signals with realtime subscriptions on the todos collection. The Todo App is basic, which I don't focus on, but I show you modern Angular techniques since Angular 16. You also see that deploying your app on serverless is very easy, but Angular with Firebase cannot be deployed on the Edge without using the AnalogJS Framework.
Environment Variables#
The old Angular way is to use an environment.ts
file to configure your app. We now know this is bad practice. We don’t want our keys, even public keys, easily accessible on GitHub. The easiest way to allow .env
file support in Angular 16 and up, is to use @ngx-env/builder. You could also consider using Webpack, but not as easy to configure.
ng add @ngx-env/builder
Note: If you’re using AnalogJS, you can skip this step, as .env
file support works out of the box with Vite.
You need to add the NG_APP_FIREBASE_CONFIG
variable to your .env
file. You an use your variables anywhere in your project from your .env
files as long as the prefix is NG_APP_
.
NG_APP_FIREBASE_CONFIG={"apiKey":"...","authDomain":"..."...}
Note: The keys must be in double quotes as well.
const firebaseConfig = JSON.parse(
import.meta.env['NG_APP_FIREBASE_CONFIG']
);
You can parse the data at the top of your app.config.ts
file to be used later.
AngularFire#
First you need to install AngularFire. This is necessary in Angular due to the big JavaScript wrapper called Zone.JS. This is said to be optional in future versions of Angular, and possibly removed. It creates a large overhead that is going to be unnecessary now that we have Signals.
First add the Angular Fire package.
ng add @angular/fire
Because we use standalone components, we need to put our Firebase providers in importProvidersFrom
in our app.config.ts
file so that they work correctly. You app will have all of them in separate functions, but you can just delete the generated version and simplify it to look pretty.
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideClientHydration(),
// Angular Providers
importProvidersFrom(
provideFirebaseApp(() => initializeApp(firebaseConfig)),
provideFirestore(() => getFirestore()),
provideAuth(() => getAuth()),
// provideStorage(() => getStorage()),
// provideAnalytics(() => getAnalytics()),
)
]
};
User Service#
Since we can’t view our todos without a logged in user, we must first handle the user state. First we make a new service.
ng g s services/user
We will match the User
type in Firebase, but we only need a few of the variables. We must make a type in TypeScript.
export interface userData {
photoURL: string | null;
uid: string;
displayName: string | null;
email: string | null;
};
We are basically doing a hook like pattern from other frameworks, but the Angular way. While the old way of using rxjs
declaratively still works, Signals will be much cleaner and faster. If we were fetching async data directly, we would probably prefer the rxjs
way.
Signals#
First we create a signal in in the service to handle the data and the loading state after we inject the Auth provider from Firebase.
private auth = inject(Auth);
user$ = signal<{
loading: boolean,
data: userData | null
}>({
loading: true,
data: null
});
Next we subscribe onIdTokenChanged
, which will watch token and user changes. If we were using an rxjs
version, or we didn’t want to change the shape of the user variable, we would probably use user
variable from @angular/fire/auth
. Either way, this version uses Signals. To keep watch of changes and to subscribe the signal way, we put the subscription in effect()
. This is the same pattern you see with hooks in other frameworks.
effect(() => {
// toggle loading
this.user$().loading = true;
// server environment
if (!this.auth) {
this.user$().loading = false;
this.user$().data = null;
return;
}
return onIdTokenChanged(this.auth, (_user: User | null) => {
this.user$().loading = false;
if (!_user) {
this.user$().data = null;
return;
}
// map data to user data type
const { photoURL, uid, displayName, email } = _user;
const data = { photoURL, uid, displayName, email };
// print data in dev mode
if (isDevMode()) {
console.log(data);
}
// set store
this.user$().data = data;
});
});
Login#
Finally, we need to add our login methods to the service. This app uses Login With Google to keep it simple.
login() {
signInWithPopup(this.auth, new GoogleAuthProvider());
}
logout() {
signOut(this.auth);
}
Todo Service#
Generate the service.
ng g s services/todos
This service follows the same pattern as our user service, except that we require the user auth to subscribe to the todos collection by injecting the user service.
user = inject(UserService).user$;
db = getFirestore();
todos = signal<{
data: TodoItem[],
loading: boolean
}>({
data: [],
loading: true
});
We put the realtime query in our effect()
function as well, and it will automatically unsubscribe when the component is destroyed by returning the snapshot.
effect(() => {
const userData = this.user().data;
if (!userData) {
this.todos().loading = false;
this.todos().data = [];
return;
}
return onSnapshot(
// query realtime todo list
query(
collection(this.db, 'todos') as CollectionReference<TodoItem[]>,
where('uid', '==', userData.uid),
orderBy('created')
), (q) => {
// toggle loading
this.todos().loading = false;
// get data, map to todo type
const data = snapToData(q);
/**
* Note: Will get triggered 2x on add
* 1 - for optimistic update
* 2 - update real date from server date
*/
// print data in dev mode
if (isDevMode()) {
console.log(data);
}
// add to store
this.todos().data = data;
});
});
If there is no user, we don’t subscribe the database. This is important as the where
clause requires a user id. We also need to transform the snap shot data, so I added a helper function above the service. You can put it in an external file if you plan to reuse it.
export const snapToData = (
q: QuerySnapshot<DocumentData, DocumentData>
) => {
// creates todo data from snapshot
if (q.empty) {
return [];
}
return q.docs.map((doc) => {
const data = doc.data();
return {
...data,
created: new Date(data['created']?.toMillis()),
id: doc.id
}
}) as TodoItem[];
}
This just keeps everything clean.
Todo CRUD#
My example app doesn’t use Angular Reactive Forms, but you would probably want to if you have anything but simplicity.
addTodo = (e: SubmitEvent) => {
e.preventDefault();
if (!this.user().data) {
throw 'No user!';
}
const uid = this.user().data?.uid;
// get and reset form
const target = e.target as HTMLFormElement;
const form = new FormData(target);
const { task } = Object.fromEntries(form);
if (typeof task !== 'string') {
return;
}
// reset form
target.reset();
addDoc(collection(this.db, 'todos'), {
uid,
text: task,
complete: false,
created: serverTimestamp()
});
}
You must prevent the todo from being added if there is no user, but this should be impossible as you shouldn’t be able to view the form without a logged in user. However, errors can happen.
The update and delete functions are as expected.
updateTodo = (id: string, complete: boolean) => {
updateDoc(doc(this.db, 'todos', id), { complete });
}
deleteTodo = (id: string) => {
deleteDoc(doc(this.db, 'todos', id));
}
Components#
I’m not going to go over the entire Angular app as this post is more about Firebase, but you need to know that you can use the user service in any component by injecting it.
us = inject(UserService);
Or if you need just the user signal:
user = inject(UserService).user$;
You call it in your template with:
us.user$().data
And of course you do the same thing with todos.
todos = inject(TodosService).todos;
Just make sure to put the todos in a component where a user is logged in.
Deployment#
Deploying an Angular app is not easy, but deploying an Angular Fire app is even more brutal. Google wants you to use Firebase Functions of course. You can use firebase init hosting
or ng deploy
and follow the instructions. You may need to set prerender
, to false
in angular.json
, and comment out the port in server.ts
, as this is a known issue.
// const port = process.env['PORT'] || 4000;
const port = 4000;
See State of Angular SSR Deployment for other serverless options and more information.
Edge Deployment#
There is no efficient way to deploy Angular on the Edge without using AnalogJS. For the moment, that is the only way to get Angular and Firebase to work together. You also cannot use firebase-admin
at all on Edge servers, but you could theoretically call a Firebase Cloud Function to handle the login from the server.
In any case, remember to add your firebase config information to your production server.
Repo: GitHub
Analog Repo: GitHub
Serverless Demo: Vercel Demo
Edge Demo: Netlify Demo