I Rewrote Angular Component Store with Signals – And Cut the Complexity in Half

After 8+ years working with Angular, I thought I had state management figured out.
Than I rewrote one of my real-world stores using Signals, because I realize I was mostly managing complexity, not reducing it.

The Context

Like many Angular …


This content originally appeared on DEV Community and was authored by Simone Boccato

After 8+ years working with Angular, I thought I had state management figured out.
Than I rewrote one of my real-world stores using Signals, because I realize I was mostly managing complexity, not reducing it.

The Context

Like many Angular developers, I've used NgRx for years. When ComponentStore came out, it felt like the perfect balance:

  • Local state
  • Reactive patterns
  • Powerfull async handling

In this case, I was working on a fairly standard feature:

  • Load user data
  • Manage countries, provinces, cities
  • Handle cascading selections

Nothing fancy - but not trivial either.

At some point, I stopped and asked myself:

Am I solving complexity... or just managing it better ?

So I tried something simple: rewrite the same store using Signal Store (Angular 19+).

Component Store version

Following the standard pattern, we define:

  • Selectors
  • Updaters
  • Effects

Selectors

readonly cities$: Observable<Record<SectionGeographicalType, City[]>> = this.select(
  (state: PersonalDataState) => state.cities,
);

readonly personalDataInfo$: Observable<PersonalDataInfo | null> = this.select(
  (state) => state.personalDataInfo
);

Updaters

readonly setCityList = this.updater((state: PersonalDataState, updateCities: UpdateCities) => {
  return {
    ...state,
    cities: {
      ...state.cities,
      [updateCities.type]: updateCities.cities,
    },
    error: null,
  };
});

Effects

readonly selectedCountry = this.effect((selectCountry$: Observable<SelectCountryModel>) => {
  return selectCountry$.pipe(
    switchMap((selectCountry: SelectCountryModel) => {
      return forkJoin([
        this.countryService.getProvinceList(selectCountry.country.sk),
        of(selectCountry.type),
      ]);
    }),
    map(([response, type]) => {
      this.setCityList({ type, cities: [] });

      if (!response?.success) {
        this.setProvinceList({ type, provinces: [] });
        return EMPTY;
      }

      this.setProvinceList({ type, provinces: response.data ?? [] });
      return EMPTY;
    }),
    catchError((error: any) => {
      return selectCountry$.pipe(
        tap((selectCountry: SelectCountryModel) => {
          this.setCityList({ type: selectCountry.type, cities: [] });
          this.setProvinceList({ type: selectCountry.type, provinces: [] });
        }),
        map(() => EMPTY),
      );
    }),
  );
});

What's the Problem ?

Nothing. This is correct, scalable and idiomatic RxJS.
But here's the issue:

It's harder to read than it needs to be for this level of complexity

To understand the flow, you need to:

  • Mentally simulate streams
  • Jump between effects, updaters and selector
  • Track async behavior across operators

That's a cognitive cost.

Rewrite to Signal Store

export const PersonalDataStore = signalStore(
  { providedIn: 'root' },
  withState(initialPersonalDataState),
  withMethods(
    (
      store,
      countryService: CountriesService = inject(CountriesService),
      logger: LoggerService = inject(LoggerService),
      personalDataService: PersonalDataService = inject(PersonalDataService),
      serviceHttpService: ServiceHttpService = inject(ServiceHttpService),
      userHttpService: UserHttpService = inject(UserHttpService),
    ) => {
      const _updateCityList = (updateCities: UpdateCities) => {
        patchState(store, {
          cities: {
            ...store._cities(),
            [updateCities.type]: updateCities.cities,
          },
        });
      };

      const _updateProvinceList = (updateProvinces: UpdateProvinces) => {
        patchState(store, {
          provinces: {
            ...store._provinces(),
            [updateProvinces.type]: updateProvinces.provinces,
          },
        });
      };

      const _updateUser = (user: PersonalDataInfo | null) => {
        patchState(store, { personalDataInfo: user });
      };

      const loadInitialData = async (): Promise<void> => {
        const user: Response<User> = await lastValueFrom(userHttpService.getUser());
        const personalDataInfo: PersonalDataInfo | null = personalDataService.convertUserToFormModel(user.data);
        _updateUser(user.success ? personalDataInfo : null);
        const countries = await lastValueFrom(countryService.countries$);
        patchState(store, { countries: countries });
      };

      const selectedCountry = async (selectCountry: SelectCountryModel): Promise<void> => {
        try {
          _updateCityList({ type: selectCountry.type, cities: [] });
          const response = await lastValueFrom(serviceHttpService.getProvinceList(selectCountry.country.sk));

          if (!response?.success) {
            _updateProvinceList({ type: selectCountry.type, provinces: [] });
          } else {
            _updateProvinceList({ type: selectCountry.type, provinces: response.data ?? [] });
          }
        } catch (e) {
          _updateCityList({ type: selectCountry.type, cities: [] });
          _updateProvinceList({ type: selectCountry.type, provinces: [] });
        }
      };

      const selectedProvince = async (selectProvince: SelectProvinceModel) => {
        try {
          const response = await lastValueFrom(
            serviceHttpService.getCityList(selectProvince.province.countrySk, selectProvince.province.code),
          );
          if (!response?.success) {
            _updateCityList({ type: selectProvince.type, cities: [] });
          } else {
            _updateCityList({ type: selectProvince.type, cities: response.data ?? [] });
          }
        } catch (e) {
          _updateCityList({ type: selectProvince.type, cities: [] });
        }
      };

      return {
        loadInitialData,
        selectedCountry,
        selectedProvince,
      };
    },
  ),
);

export type PersonalDataStore = InstanceType<typeof PersonalDataStore>;

The Real Difference

This isn't about syntax. it's about how your brain process the code.
There are:

  • No streams to simulate
  • No operators no mentally execute
  • No indirection between layers

Just:

  • perform an action
  • update the state

The Trade-Off

This rewrite is not "free".
I intentionally moved from reactive streams to imperative async flows.

What I lost:

  • Built-in cancellation (e.g switchMap)
  • Stream composition
  • Reactive coordination across multiple sources

What I gained:

  • Linear, readable logic
  • Easier onboarding
  • Lower cognitive overhead

And for this feature, that trade-off was worth it.

When ComponentStore Still Wins

There are cases where RxJS is absolutely the right tool:

  • Complex async orchestration
  • Race conditions and cancellation
  • WebSocket or event streams
  • Combining multiple reactive sources

In those scenarios, Signals won't replace RxJS - they complement it.

Final Thought

We didn't remove reactivity.
We just chose a simpler model for a problem that didn't need the full power of RxJS, and in doing that, we reduce the cognitive load without sacrificing the outcome


This content originally appeared on DEV Community and was authored by Simone Boccato


Print Share Comment Cite Upload Translate Updates
APA

Simone Boccato | Sciencx (2026-04-17T19:49:46+00:00) I Rewrote Angular Component Store with Signals – And Cut the Complexity in Half. Retrieved from https://www.scien.cx/2026/04/17/i-rewrote-angular-component-store-with-signals-and-cut-the-complexity-in-half/

MLA
" » I Rewrote Angular Component Store with Signals – And Cut the Complexity in Half." Simone Boccato | Sciencx - Friday April 17, 2026, https://www.scien.cx/2026/04/17/i-rewrote-angular-component-store-with-signals-and-cut-the-complexity-in-half/
HARVARD
Simone Boccato | Sciencx Friday April 17, 2026 » I Rewrote Angular Component Store with Signals – And Cut the Complexity in Half., viewed ,<https://www.scien.cx/2026/04/17/i-rewrote-angular-component-store-with-signals-and-cut-the-complexity-in-half/>
VANCOUVER
Simone Boccato | Sciencx - » I Rewrote Angular Component Store with Signals – And Cut the Complexity in Half. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2026/04/17/i-rewrote-angular-component-store-with-signals-and-cut-the-complexity-in-half/
CHICAGO
" » I Rewrote Angular Component Store with Signals – And Cut the Complexity in Half." Simone Boccato | Sciencx - Accessed . https://www.scien.cx/2026/04/17/i-rewrote-angular-component-store-with-signals-and-cut-the-complexity-in-half/
IEEE
" » I Rewrote Angular Component Store with Signals – And Cut the Complexity in Half." Simone Boccato | Sciencx [Online]. Available: https://www.scien.cx/2026/04/17/i-rewrote-angular-component-store-with-signals-and-cut-the-complexity-in-half/. [Accessed: ]
rf:citation
» I Rewrote Angular Component Store with Signals – And Cut the Complexity in Half | Simone Boccato | Sciencx | https://www.scien.cx/2026/04/17/i-rewrote-angular-component-store-with-signals-and-cut-the-complexity-in-half/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.