Skip to content

Creating a Messages Service to Interact with Firestore

Once again, our usual approach is to start with the “main” feature of the application. Although we will eventually have things like login and account creation, the main purpose of our application is creating and displaying messages. Let’s start there.

Create an Interface for messages

export interface Message {
author: string;
content: string;
created: string;
}

Creating the Message Service

import { Injectable, computed, inject, signal } from '@angular/core';
import { Observable, merge } from 'rxjs';
import { collection, query, orderBy, limit } from 'firebase/firestore';
import { collectionData } from 'rxfire/firestore';
import { map } from 'rxjs/operators';
import { connect } from 'ngxtension/connect';
import { Message } from '../interfaces/message';
import { FIRESTORE } from '../../app.config';
interface MessageState {
messages: Message[];
error: string | null;
}
@Injectable({
providedIn: 'root',
})
export class MessageService {
private firestore = inject(FIRESTORE);
// sources
messages$ = this.getMessages();
// state
private state = signal<MessageState>({
messages: [],
error: null,
});
// selectors
messages = computed(() => this.state().messages);
error = computed(() => this.state().error);
constructor() {
// reducers
const nextState$ = merge(
this.messages$.pipe(map((messages) => ({ messages })))
);
connect(this.state).with(nextState$);
}
private getMessages() {
const messagesCollection = query(
collection(this.firestore, 'messages'),
orderBy('created', 'desc'),
limit(50)
);
return collectionData(messagesCollection, { idField: 'id' }).pipe(
map((messages) => [...messages].reverse())
) as Observable<Message[]>;
}
}

Once again we have our basic state management setup, except this time we are using the connect function that we covered in the advanced state management module.

To quickly recap, this is essentially the same idea as our normal reducers that we have been creating:

constructor() {
// reducers
const nextState$ = merge(
this.messages$.pipe(map((messages) => ({ messages })))
);
connect(this.state).with(nextState$);
}

Except rather than subscribing and calling state.update with the source values, instead we map those values and return an object with whatever values we want to set in the state. In this case, we return { messages } because we want to update the messages property in our state signal with the messages emitted on the messages$ stream.

Notice that we are also already integrating with Firestore now and making use of our injection token by injecting FIRESTORE as this.firestore:

private getMessages() {
const messagesCollection = query(
collection(this.firestore, 'messages'),
orderBy('created', 'desc'),
limit(50)
);
return collectionData(messagesCollection, { idField: 'id' }).pipe(
map((messages) => [...messages].reverse())
) as Observable<Message[]>;
}

We are using some functions from both the firebase and rxfire libraries in order to retrieve documents from a specific collection in our Firestore database. Let’s walk through what is happening step-by-step here:

The goal here is that we want this to return an observable stream of the last 50 messages that have been added to our database. First, we could just do this to get a reference to a specific collection:

const messagesCollection = collection(this.firestore, 'messages')

This will give us a reference to the messages collection of documents in our Firestore database (we haven’t actually added any collections or documents to the database, we will do that shortly).

But, we don’t just want to get everything from that collection, we want to query for specific documents, which is why we use a query instead:

const messagesCollection = query(
collection(this.firestore, 'messages'),
orderBy('created', 'desc'),
limit(50)
);

Now, we will only return 50 documents which are ordered by their created date in descending order. Creating this query won’t actually give us the stream of data we want though, we need to supply it to the collectionData method and return that:

return collectionData(messagesCollection, { idField: 'id' }).pipe(
map((messages) => [...messages].reverse())
) as Observable<Message[]>;

This collectionData method is from rxfire not firebase — we want this data returned as an observable stream, which is why we use the rxfire method.

We also supply a couple of configurations for this — we want to get the unique id from our documents so we supply the idField configuration, and we also want the messages in reverse order (the latest should be at the bottom of the screen) so we map and reverse them.

Now we can just call our getMessages method and we will get a stream of the latest 50 messages from our Firestore database that will automatically update in real time whenever a new message is added to the database!

Displaying Messages

Now that we have a way to get messages, we need a way to display them. We should be getting back to familiar territory now, as we will be creating a simple dumb component to display the data we get back from the database.

Go ahead and see if you can create this component — it should accept an array of messages that matches the Message interface we created and it should render out an item for each one. We will add some specific stuff for styling in a moment, just try to get the basic skeleton set up.

import { Component, input } from '@angular/core';
import { Message } from '../../shared/interfaces/message';
@Component({
selector: 'app-message-list',
template: `
<ul class="gradient-bg">
@for (message of messages(); track message.created){
<li>
<div class="avatar animate-in-primary">
<img
src="https://api.dicebear.com/7.x/bottts/svg?seed={{
message.author.split('@')[0]
}}"
/>
</div>
<div class="message animate-in-secondary">
<small>{{ message.author }}</small>
<p>
{{ message.content }}
</p>
</div>
</li>
}
</ul>
`,
})
export class MessageListComponent {
messages = input.required<Message[]>();
}

One fun thing we are doing here is this:

<img
src="https://api.dicebear.com/7.x/bottts/svg?seed={{
message.author.split('@')[0]
}}"
/>

We are using the dicebear API here to return funky random avatars for us. The idea is that you just pass a unique id to the API and it will return an avatar for that id. We are just using the author’s email address to identify them for the avatar.

Now we are going to use our MessageListComponent in our HomeComponent to display a list of data from Firebase. See if you can do that.

import { Component, inject } from '@angular/core';
import { MessageListComponent } from './ui/message-list.component';
import { MessageService } from '../shared/data-access/message.service';
@Component({
selector: 'app-home',
template: `
<div class="container">
<app-message-list [messages]="messageService.messages()" />
</div>
`,
imports: [MessageListComponent],
})
export default class HomeComponent {
messageService = inject(MessageService);
}

This should all be working at this point… only we don’t have any data yet! Let’s add some test data to our emulators to see if it is all working.

Adding Data to the Emulator

First, make sure you run the start script:

Terminal window
npm start

This will ensure that the emulators are started before serving your application. Next, you can head over to:

http://localhost:4000

From there, click on the Firestore tab. You should see an option that says + Start collection. Click that, and create a collection with a Collection ID of messsages.

This will automatically create our first document for us, but we need to make sure to fill out its fields. Make sure to add the following fields and values when creating a document:

author | string | Josh
content | string | hello
created | string |

Adding data to Firestore emulator

You can just leave the created blank — we will create actual dates later but that isn’t really required for testing. If you have your application open in another window, you should see this new data instantly pop in when you add it.

The styling is a bit funky right now (you know… on account of the whole giant robot head thing) — we will work on that later. If you like, you can add some additional documents as well if you want — just keep in mind that when you stop the emulators all of the data will be deleted!