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.
NOTE: Just a reminder that for all of the application stuff that we have already covered throughout the course, I am going to go very light on the details/directions in this application build. This is to give you a chance to apply what you have learned, highlight areas you might need to focus on more, and hopefully give you a better shot at creating applications independently once we are done with this course. However, there will be a lot of new things introduced in this module, and we will still talk about those in detail
Create an Interface for messages
Create a new file at
src/app/shared/interfaces/message.ts
and add the following:
export interface Message {
author: string;
content: string;
created: string;
}
Creating the Message Service
Create a new file at
src/app/shared/data-access/message.service.ts
and add the following:
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[]>;
}
}
IMPORTANT: Currently, there is an issue when using rxfire
with the
moduleResolution
option set to bundler
(which is the default). If you
experience an error when trying to import collectionData
update the following
field in your tsconfig.json
file from:
"moduleResolution": "bundler",
to:
"moduleResolution": "node",
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
: