Category: angular

  • How Not to Use Observables & Signals

    How Not to Use Observables & Signals

    Below we have some problem code, this came from a real codebase (names changed here to protect the original code); this code just stinks. I’ve removed the imports for clarity.

    This is certainly a memory jogger for me on how bad things can get. :/ The walk through is just below this code.

    @Injectable({ providedIn: 'root' })
    export class SubjectSignalDataService implements OnDestroy {
      readonly #subs = new Subscription();
      readonly #loading = signal<boolean>(true);
      readonly #subjects = signal<ISubject[]>([]);
      readonly #clusters = signal<ISubjectWithCluster[]>([]);
    
      // Exposed computed streams
      itemsList = computed(() =>
        this.#subjects().map(item => this.#attachPrimaryMember(item))
      );
      isInitialized = computed(() => !this.#loading());
    
      constructor(
        private hubA: HubServiceA,
        private hubB: HubServiceB,
        private profile: ProfileService
      ) {
        this.#subs.add(
          this.hubA.payloadStream$.subscribe((payload: IDataPayload) => {
            const mapped = this.#mapRecords(payload.sources);
            this.#subjects.set(mapped);
            this.#loading.set(false);
          })
        );
    
        this.#subs.add(
          this.hubB.dataWithClusters$.subscribe(
            (clusters: ISubjectWithCluster[]) => {
              const filtered = clusters.filter(
                c => c.ownerId === this.profile.currentUser().id
              );
              this.#clusters.set(filtered);
            }
          )
        );
      }
    
      ngOnDestroy() {
        this.#subs.unsubscribe();
      }
    
      #mapChats(chats: Record<string, IChat>): IChat[] {
        return Object.values(chats).map(c => ({
          chatId: c.chatId,
          channel: c.channel,
          startedAt: new Date(c.startedAt),
          endedAt: c.endedAt ? new Date(c.endedAt) : null,
          participants: c.participants,
          metadata: c.metadata,
        }));
      }
    
      #mapRecords(sources: Record<string, ISubject>): ISubject[] {
        return Object.values(sources).map(src => ({
          ...src,
          clusters: this.#mapChats(src.clusters as any),
        } as ISubject));
      }
    
      #attachPrimaryMember(item: ISubject): ISubject {
        const cluster = this.#clusters().find(c => c.id === item.id);
        const primary = cluster?.items.find(i => i.id === cluster.primaryItemId);
        return { ...item, primaryMember: primary?.member };
      }
    }
    

    Screenshots from VS code

    My Walk Through of the Bad Code

    Class declaration

    On the very next line, seeing implements “OnDestroy” in a singleton service sets off my Spidey-sense. In Angular, a root-provided service lives for the app’s lifetime, so its “ngOnDestroy” will never run—and you shouldn’t rely on it there.

    Subscription field

    Then we have readonly #subs = new Subscription(). Whenever I see a raw Subscription in a service, I assume something down below is wrong—manual subscription management in a singleton is almost always a code smell.

    Private signals

    Next come the private signals: #subject, #clusters, #loading. My understanding is that signals are meant to drive template reactivity—so keeping them private in a service feels off. Why not just expose Observables?

    Computed streams

    After that, we see the exposed computed fields (itemsList and isInitialised). These are hooked to the private signals, so they “work,” but computing inside the service like this (based on data set by hidden subscriptions) is unnecessary coupling.

    Constructor subscriptions

    In the constructor, two .subscribe(…) calls silently wire up hubA.payLoadStream$ and hubB.dataWithClusters$. Hiding subscriptions here makes it hard to reason about when or where data flows. I’d rather expose the raw streams and subscribe in a clear, visible component (e.g. AppComponent) after the service is instantiated.

    ngOnDestroy

    Here’s the ngOnDestroy() that unsubscribes #subs. But as noted above, in a root singleton this hook never actually fires—so it’s dead code.

    Private helper methods

    Finally, the private transformers (#mapChatsAndRecords, #attachPrimaryMember) do the data enrichment. Aside from their own complexity, tucking all this logic behind signals and subscriptions makes the service hard to follow.

    Overall, the service mixes concerns—manual RxJS subscriptions, private signals, and computed logic—in a way that’s neither clear nor maintainable. I’d suggest refactoring toward plain Observables, exposing transformation pipelines, and handling subscription lifecycles in components rather than in a singleton service.

    The Way I Would Approach This Code

    Service with just signals in (still needs more work)

    
    @Injectable({ providedIn: 'root' })
    export class SubjectSignalService {
      readonly #loading = signal<boolean>(true);
      readonly #subjects = signal<ISubject[]>([]);
      readonly #clusters = signal<ISubjectWithCluster[]>([]);
    
      isLoading = computed(() => this.#loading());
      isReady = computed(() => !this.#loading());
      subjects = computed(() => this.#subjects().map(sub => this.#attachMain(sub)));
    
      setSubjects(subjects: ISubject[]): void {
        this.#subjects.set(subjects);
        this.#loading.set(false);
      }
    
      setClusters(clusters: ISubjectWithCluster[]): void {
        this.#clusters.set(clusters);
      }
    
      #attachMain(subject: ISubject): ISubject {
        const cluster = this.#clusters().find(c => c.id === subject.id);
        const mainItem = cluster?.items.find(item => item.id === cluster.primaryItemId);
        return {
          ...subject,
          primaryMember: mainItem?.member,
        };
      }
    }

    Mapping logic extracted

    So far I’ve separated the observable streams from the signals, this let me move two of the private mapping methods out of this service (they simply transform incoming data).

    Private signals remain

    I haven’t cleaned up the private signals (#loading, #subjects, #clusters), but remember: signals replace zone.js change detection and should be public so templates can consume them.

    Setter methods for data flow

    I added setSubjects and setClusters methods, these are now called from the app component (or other orchestrating code) after subscribing to the extracted observable sources.

    Second Service

    This is the service that I have moved the observables into, note the naming convention, same name as the first service but just with “Data” appended.

    @Injectable({ providedIn: 'root' })
    export class SubjectSignalDataService {
      readonly #records$ = new BehaviorSubject<IDataRecord[]>([]);
      readonly #groups$ = new BehaviorSubject<IDataRecordWithGroup[]>([]);
    
      /** Public streams of the latest data */
      records$ = this.#records$.asObservable();
      recordsWithGroups$ = this.#groups$.asObservable();
    
      constructor(
        private hubA: HubServiceA,
        private hubB: HubServiceB,
        private profile: ProfileService
      ) {}
    
      /**
       * Stream of grouped records filtered by current user, updates internal subject.
       */
      getFilteredRecordsWithGroups$(): Observable<IDataRecordWithGroup[]> {
        return this.hubB.dataWithGroups$.pipe(
          map(arr => arr.filter(g => g.ownerId === this.profile.currentUser().id)),
          tap(arr => this.#groups$.next(arr))
        );
      }
    
      /**
       * Stream of transformed records, updates internal subject.
       */
      getTransformedRecords$(): Observable<IDataRecord[]> {
        return this.hubA.agentState$.pipe(
          map((state: IAgentState) => this.#mapRecords(state.sources)),
          tap(items => this.#records$.next(items))
        );
      }
    
      /**
       * Convert raw chat records into an array of IChat
       */
      #mapChats(chats: Record<string, IChat>): IChat[] {
        return Object.values(chats).map(c => ({
          chatId: c.chatId,
          channel: c.channel as ChannelKind,
          startedAt: new Date(c.startedAt),
          endedAt: c.endedAt ? new Date(c.endedAt) : null,
          participants: c.participants,
          metadata: c.metadata,
        }));
      }
    
      /**
       * Convert raw data sources into IDataRecord array
       */
      #mapRecords(sources: Record<string, IDataRecordSource>): IDataRecord[] {
        return Object.values(sources).map(src => ({
          recordId: src.sourceId,
          chats: this.#mapChats(src.chats),
          categoryId: src.categoryId ?? null,
          userId: src.userId ?? null,
          state: src.state as RecordState,
          createdOn: new Date(src.createdOn),
          mainChatId: src.mainChatId,
          tags: src.tags,
          mediaType: src.mediaType as MediaKind,
          isActive: src.isActive,
          notes: src.notes,
          primaryOwner: undefined,
        }));
      }
    }

    BehaviorSubjects for further chaining

    BehaviorSubjects so that other features can chain or map this data downstream without relying on the service with the signal data, I think mapping of the observables and creating a service per feature for the signal data helps a lot for readability.

    Screenshots from VS code with correct formatting.

    Summary

    I would simplify SubjectSignalService further. You do not need both isLoading and isReady if you structure your data flow correctly. Since you now call the SubjectSignalDataService observables directly in the places where data is needed, you can manage a single loading flag there. When you subscribe and receive data, set your loading flag to true; once you call the signal setters, set it back to false.

    The original service misused signals in several ways. Because its private signals were initialized with empty arrays, developers elsewhere began calling toObservable() on a public signal that was still empty. I’ll write a few short posts to demonstrate these issues and show how to avoid them.

    P.S. I apologise if any of the code names do not match, I tried to squeeze the creation on this post in between patting our twin babies to sleep, screams and gurgles, across several days, etc, I hope you catch my drift, if you have children, you will. 🙂

  • MVP: What’s In, What’s Out — And Why I Still Use Facades

    Intro – Inspired by a PR Comment

    This post was triggered by a PR comment: ‘Isn’t this wrapper overkill for MVP?’ I’ve been there before — and I’ve learned that a little early structure saves a lot of late pain.

    Why I Stick to Facades and Wrappers From the Start

    Even when moving quickly, I find it worth using:

    • Per-feature facades** (e.g. ChatFacade, EmailFacade)
    • Per-hub gateway services** (e.g. ChatHubGateway, EmailHubGateway)
    • DTO mappers

    They:

    • Add barely any overhead at the start.
    • Keep backend transport concerns (SignalR/HTTP) isolated.
    • Make future changes predictable — especially when multiple hubs are involved.
    • Save me from pulling tangled logic out of components later.

    Even if the app doesn’t go far, it’s a minimal investment for peace of mind.

    What I Include Early (Post-MVP Stability)

    1. Per-Hub Gateway Services

    Each hub (chat, email, agent) gets its own wrapper for connection and event handling.

    @Injectable({ providedIn: 'root' })
    export class ChatHubGateway {
      #conn = new HubConnectionBuilder().withUrl('/chatHub').build();
      #msg$ = new Subject<ChatMessage>();
      message$ = this.msg$.asObservable();
    
      start() {
        this.conn.on('ReceiveMessage', m => this.msg$.next(m));
        return this.conn.start();
      }
    }

    2. Typed DTO Mapping

    I always keep backend shapes away from UI models.

    toChatMessage(dto: ChatMessageDto): ChatMessage {
      return { sender: dto.from, text: dto.content, timestamp: new Date(dto.ts) };
    }

    3. Connection Status Signals

    @Injectable({ providedIn: 'root' })
    export class EmailHubService extends SignalRHubService {
      readonly status = signal<'connected' | 'disconnected' | 'reconnecting'>('disconnected');
    
      override startConnection() {
        this.hubConnection.onreconnecting(() => this.status.set('reconnecting'));
        this.hubConnection.onreconnected(() => this.status.set('connected'));
        this.hubConnection.onclose(() => this.status.set('disconnected'));
        return this.hubConnection.start();
      }
    }

    Quick Note

    Currently my understanding about signals is that they should only be used when data is required in the template, so the “status” above should only be used for informing the end user, I would ideally separate out observable and signal data into different services, I have been using naming convention like “emailHubDataService” for all observable things and then “emailHubService” for any signal based things.

    In a component:

    @Component({ ... })
    export class EmailPanelComponent {
      readonly connectionStatus = inject(EmailHubService).status;
    }

    Now you can show:

    html
    @if(isReconnecting)
    <div>
      Attempting to reconnect...
    </div>
    
    ts
    const status = signal<'connected' | 'disconnected' | 'reconnecting'>('disconnected');
    
    isReconnecting = computed(() => status() === 'reconnecting')

    Tracking Incoming vs Outgoing Traffic

    It helps to distinguish what’s being sent to the server vs what’s coming from the server. I’ve found it useful to separate these both semantically and in logging.

    This is an area I have recently made some mistakes in and was informed that I had over engineered, (hence the inspiration for this post) I added a wrapper around the emailHubService and than added two new services to distinguish between incoming and outgoing calls, I understand now, that it was over engineered, my understanding just came from understanding the original hub services.

    Semantic distinction:

    An example of how what I wanted to achieve can be done without the separate services.

    // OUTGOING (client → server)
    sendDisconnect(...)
    sendAcceptEmail(...)
    senRejectEmail(...)
    
    // INCOMING (server → client)
    onReceiveEmailMessage(...)
    registerHandlers() // binds handlers like 'ReceiveEmailMessage'

    Logging:

    Wrap both directions to log clearly:

    // Outgoing
    send<T>(method: string, payload: T) {
      this.logger.debug(`[OUTGOING] ${method}`, payload);
      this.hubConnection?.send(method, payload);
    }
    
    // Incoming
    #handleIncoming<T>(label: string, payload: T) {
      this.logger.debug(`[INCOMING] ${label}`, payload);
    }

    This makes tracing issues between frontend and backend a lot easier, especially when events stop flowing or are being sent with unexpected payloads.

    Side Note:

    The use of the “#” syntax in place of the “private” access modifier.

    What I Leave Until Later

    I think these should wait until there’s a clear need:

    • Global state libraries (NgRx, Akita)
    • Factories for creating hubs
    • Generic event buses
    • Central hub connection manager (unless coordinating 3+ hubs)

    Observables First, Signals Later (Reminder to Self)

    A quick personal rule:

    Keep SignalR data as observables until it reaches the DOM — then convert to signals, if the template has a service, then I feel it is fine to convert it there too, just as long as it is not being reference around the rest of the codebase.

    Why?

    • Observables are better for streaming, retries, and cancellations.
    • Signals are great for UI reactivity.
    • This keeps the core data flow reactive without tying it to the DOM too early.

    Typical usage:

    @Component({ ... })
    export class ChatComponent {
      readonly messageSignal = toSignal(chatHub.message$);
    }

    Final Thought

    This isn’t about gold-plating MVPs — it’s about laying groundwork that doesn’t cost much but saves me big later.

    Even if nothing ships, I’d rather have clean wrappers and small abstractions than spend hours later undoing a spaghetti mess. If it all falls over? At least I didn’t build the mess twice.

  • Simple Jest Snapshot for Angular Component

    Simple Jest Snapshot for Angular Component

    Below is a simple jest snapshot with an interface for the list items and three signal inputs.

    Points to Note

    • The “render” function comes from the “@testing-library/angular”.
    • Not passing the second argument may cause lint errors or miss leading co-pilot suggestions.
    • Using the “render” function allows you the nice approach of setting the inputs.
    import { render } from '@testing-library/angular';
    import { QueueListComponent } from './queue-list.component';
    
    interface QueueItem {
      id: string;
      name: string;
      isSource: boolean;
      mode: string;
    }
    
    const mockQueues: QueueItem[] = [
      {
        id: '1',
        name: 'Alice Johnson',
        isSource: true,
        mode: '',
      },
      {
        id: '2',
        name: 'Bob Smith',
        isSource: false,
        mode: '',
      },
    ];
    
    describe('QueueListComponent', () => {
      it('renders consistently', async () => {
        const { container } = await           render(QueueListComponent, {
          inputs: {
            queues: mockQueues,
            primaryLabel: 'Primary Queue',
            secondaryLabel: 'Secondary Queue',
          },
        });
    
        expect(container).toMatchSnapshot();
      });
    });
    

    Elaboration on the Points Above

    Point One

    Using the “render” function for a snapshot was the suggestion of chat-gpt, it was the alternative to using the standard testbed, testbed was recommended when needing deeper integration with services and logic.

    What Chat-GPT had to say

    Point Two

    When I first got the suggestion from GPT, it did not give me the second arg (inputs in this case) and I was getting errors telling me about my component not being recognised, I initially opted for a co-pilot suggestion and it converted the component to a string containing the selector with the inputs passed in as if I was using it in a parent component, I cannot replicate this now, the second param of the “render” function is optional either way. (I wasn’t having the best day :/ )

    Point Three

    I liked that I could just declare the required inputs as shown above with “render” function options, it seems a bit cleaner than the only way I currently now when you use the “Testbed” approach.

    Signal Input from the Class

    inputData = input.required<IItem>();

    Spec File Code to Set the Input

    fixture = TestBed.createComponent(ExamplePanelComponent);
    component = fixture.componentInstance;
    
    fixture.componentRef.setInput('inputData', mockItem('Example'));

    Last Things to Remember

    To generate the snapshot, I was running “npx jest” this should highlight that your snapshot was created in green along with tests run, etc.

    This post was generated as a memory jogger, if you’ve found it, I hope it helps you out

  • Angular Signals and Observables: A Clean Pattern for Managing State

    Introduction

    In modern Angular applications, handling reactive data streams efficiently is crucial. This pattern ensures:

    • Separation of concerns – Data fetching and UI reactivity are managed separately.
    • Optimised reactivity – Using signals to prevent unnecessary UI updates.
    • Scalability – A structured approach that can be extended with facades if needed.

    This post presents a pattern using two services:

    1. A data service that fetches and exposes data via observables.
    2. A signal-based service that converts the observable into a signal for UI components.

    Note: When I wrote this post, I was working on a team that did not want observables in components. To handle this, you could introduce a facade service that wraps everything in signals—including the startFeed observable—and expose it via a view model. The component would then trigger the data flow by calling a signal function in the template.


    1. Data Service: Handling the Data Fetching

    This service is responsible for fetching data and exposing an observable.

    Key Points:

    • startFeed() fetches data and updates the BehaviorSubject.
    • listen() exposes the current state as an observable for external consumers.

    2. Signal Service: Converting the Observable into a Signal

    This service wraps the observable into a signal and provides derived signals for specific properties.

    Key Points:

    • toSignal() creates a reactive signal from the observable.
    • Computed signals ensure only necessary properties are exposed.
    • startFeed() is exposed so it can be subscribed to in the template using async.

    3. Using the Signal Service in a Component

    The component subscribes to “ using the async pipe, ensuring it follows reactive best practices.

    Key Points:

    • start() assigns feed$ to startFeed() and binds it in the template using async.
    • dataTitle is a signal, meaning it updates automatically when new data arrives.

    Alternative Approach: Using a Facade Service

    If you don’t want observables in the component, you could introduce a facade service that exposes everything as signals—including the startFeed() observable wrapped inside a signal.

    Then, in the component:

    This approach keeps all observable logic out of the component while maintaining a clean reactive API.


    Conclusion

    This pattern provides:

    Separation of concerns – Data fetching, state management, and UI logic are cleanly divided.

    Optimised rendering – UI only updates when necessary.

    Flexibility – Works with or without observables in the component.