DEV Community

Cover image for 62% less code: NGXS vs StateAdapt 1. Angular Ionic Movies
Mike Pearson for This is Angular

Posted on

62% less code: NGXS vs StateAdapt 1. Angular Ionic Movies

YouTube video for this article

I implemented the Angular Ionic Movies app with StateAdapt and the state management code decreased by 62% from what it was using NGXS.

Angular Ionic Movies

Actions

The first way StateAdapt is more minimal is in how it expresses event sources. This is an action in NGXS:

export class AddMovie {
  static readonly type = '[Movies] Add movie';

  constructor(public payload: Movie) {}
}
Enter fullscreen mode Exit fullscreen mode

This is the same event source in StateAdapt:

addMovie$ = new Source<Movie>('addMovie$');
Enter fullscreen mode Exit fullscreen mode

State changes

This particular NGXS project was using an NGXS Labs plugin that allows you to connect actions with action handlers defined outside the state file. That looks like this:

attachAction(MovieState, AddMovie, addMovie(moviesService));
Enter fullscreen mode Exit fullscreen mode

That is pretty similar to StateAdapt's syntax:

addMovies: this.addMovieRequest.success$,
Enter fullscreen mode Exit fullscreen mode

That extra observable came from a stream that transformed addMovie$ into some standard HTTP observables using StateAdapt's HTTP utilities:

  addMovieRequest = getHttpSources(
    '[Add Movie]',
    this.addMovie$.pipe(
      switchMap(({ payload }) => {
        payload.poster = // COPIED—Don't mutate!
          payload.poster === ''
            ? 'https://in.bmscdn.com/iedb/movies/images/website/poster/large/ela-cheppanu-et00016781-24-03-2017-18-31-40.jpg'
            : payload.poster;

        return this.moviesService.addMovie(payload);
      })
    ),
    (res) => [!!res, res, 'Error']
  );
Enter fullscreen mode Exit fullscreen mode

addMovies is a pure function inside a state adapter, and it looks like this:

add: (state, movie: Movie) => [...state, movie],
Enter fullscreen mode Exit fullscreen mode

Note: add gets turned into addMovies for a reason I'll explain in the next section.

StateAdapt keeps pure functions separate from side-effects and async code as much as possible, and you'll see how that enables some awesome reusability soon.

On the other hand, the function NGXS's action maps to includes multiple concerns and side-effects:

export const addMovie =
  (moviesService: MoviesService) =>
  ({ setState }: StateContext<MoviesStateModel>, { payload }) => {
    payload.poster =
      payload.poster === ''
        ? 'https://in.bmscdn.com/iedb/movies/images/website/poster/large/ela-cheppanu-et00016781-24-03-2017-18-31-40.jpg'
        : payload.poster;
    return moviesService.addMovie(payload).pipe(
      catchError((x, caught) => {
        return throwError(() => new Error(x));
      }),
      tap({
        next: (result) => {
          setState(
            patch({
              movies: append([result])
            })
          );
        }
      })
    );
  };
Enter fullscreen mode Exit fullscreen mode

This could have been written more succinctly, but the fact that it can be nested so much (as some devs prefer) comes from how many layers of functions need to be called.

State adapters

addMovies is an awkward name, and I did not choose it. StateAdapt generated it by joining the moviesAdapter onto a higher state shape, CatalogStateModel:

export interface CatalogStateModel {
  movies: Movie[];
  movieForm: MovieForm;
  filter: Filter;
  favorites: Movie[];
}

export const catalogAdapter = joinAdapters<CatalogStateModel>()({
  movies: moviesAdapter,
  movieForm: movieFormAdapter,
  filter: createAdapter<Filter[]>()({ selectors: {} }),
  favorites: moviesAdapter
})();
Enter fullscreen mode Exit fullscreen mode

Since add is an available state change on moviesAdapter, catalogAdapter gets it as addMovies. Every state change name is split up as <first word><Property name><Rest of words>.

However, I am working on a createListAdapter function that will generate state change names like addOne and addMany, and so on, which are not quite as awkward when converted into addMoviesOne and addMoviesMany. Still a little awkward, but it's worth it.

Why?

You may have noticed that I have two references to moviesAdapter in the last code snippet. This is because while I was busy converting NGXS action handlers into RxJS operators and adapter methods I started typing something familiar inside the favoritesAdapter:

add: (state, movie: Movie) => [...state, movie],
Enter fullscreen mode Exit fullscreen mode

I realized I had already implemented that in the moviesAdapter... In fact, both favorites and movies had type Movie[]. Why shouldn't they have similar patterns of state changes and derived state?

State adapters should really be thought of as companions to data types. This is sort of like object-oriented programming, except with immutability, and messages are defined as independent, self-describing (declarative) entities instead of imperative, forward/downstream-looking commands. This makes a massive difference. This allows proper separation of concerns, whereas traditional OOP does not, for anything asynchronous.

More on this in a future article. But I'm really excited about the possibilities.

Anyway, so that's why 2 event sources and a single, simple state change in StateAdapt came from this in NGXS:

export const addMovie =
  (moviesService: MoviesService) =>
  ({ setState }: StateContext<MoviesStateModel>, { payload }) => {
    payload.poster =
      payload.poster === ''
        ? 'https://in.bmscdn.com/iedb/movies/images/website/poster/large/ela-cheppanu-et00016781-24-03-2017-18-31-40.jpg'
        : payload.poster;
    return moviesService.addMovie(payload).pipe(
      catchError((x, caught) => {
        return throwError(() => new Error(x));
      }),
      tap({
        next: (result) => {
          setState(
            patch({
              movies: append([result])
            })
          );
        }
      })
    );
  };
Enter fullscreen mode Exit fullscreen mode
export const favoriteMovie = (
  { setState }: StateContext<MoviesStateModel>,
  { payload }
) => {
  setState(
    patch({
      favorites: append([payload])
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

(Favorites were stored in localStorage, not the server.)

Could this have been reused? Yes. One thing I've noticed is that boilerplate, being repetitive itself, obscures repetitive business logic.

Other notes

Forms plugin

NGXS had a forms plugin it was using. StateAdapt had to code this from scratch, which added a few lines.

Local storage plugin

StateAdapt doesn't have a local storage plugin yet, so I had to define a custom state sanitizer like this:

import { actionSanitizer, stateSanitizer } from '@state-adapt/core';
import { provideStore } from '@state-adapt/angular';

const enableReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION__?.({
  actionSanitizer,
  stateSanitizer: (state: any) => {
    const newState = stateSanitizer(state);
    localStorage.setItem('@@STATE', JSON.stringify(newState));
    return newState;
  }
});

export const storeProvider = provideStore(enableReduxDevTools);
Enter fullscreen mode Exit fullscreen mode

Normally this would just be

import { defaultStoreProvider } from '@state-adapt/angular';
Enter fullscreen mode Exit fullscreen mode

Imperative components

There was so much refactoring I wanted to do in the components... the Angular ecosystem really sucks at providing declarative tools. For example, declarative dialogs are almost completely lacking:

Framework Library 1 Library 2 Library 3
Vue ✅ Declarative ✅ Declarative ✅ Declarative
React ✅ Declarative ✅ Declarative ✅ Declarative
Svelte ✅ Declarative ✅ Declarative ✅ Declarative
Preact ✅ Declarative ✅ Declarative ✅ Declarative
Ember ✅ Declarative ✅ Declarative ✅ Declarative
Lit ✅ Declarative ✅ Declarative ✅ Declarative
SolidJS ✅ Declarative ✅ Declarative ---
Alpine ✅ Declarative --- ---
Angular ❌ Imperative ❌ Imperative ❌ Imperative

(I made a wrapper for Angular Material's dialog component that you should go copy and paste into your project right now.)

The result was a lot of ugly imperative code in the component:

    this.actions$.pipe(ofActionSuccessful(AddMovie)).subscribe({
      next: () => {
        this.modalCtrl.dismiss();
        this.iziToast.success('Add movie', 'Movie added successfully.');
      },
      error: (err) =>
        console.log(
          'HomePage::ngOnInit ofActionSuccessful(AddMovie) | method called -> received error' +
            err
        )
    });
Enter fullscreen mode Exit fullscreen mode

The way this would typically be done in a framework that encourages sanity, is something like this:

  • Event fires
  • State changes
  • DOM reflects state

There were similar atrocities made convenient by NGXS's dispatch() returning an observable to allow easy non-unidirectional logic:

    this.store
      .dispatch(new FetchMovies({ start: start, end: end }))
      .pipe(withLatestFrom(this.movies$))
      .subscribe({
        next: ([movies]) => {
          setTimeout(() => {
            this.showSkeleton = false;
          }, 2000);
        },
        error: (err) =>
          console.log(
            'HomePage::fetchMovies() | method called -> received error' + err
          )
      });
Enter fullscreen mode Exit fullscreen mode

showSkeleton sounds a lot like a loading state... Apparently this entire component is an action handler. This is how your code would look without a state management library.

There isn't any reliable unidirectionality in this app. The amount of freedom taken in this codebase (and in other NGXS projects I've seen) results in basically no guarantees of where you could find the cause of any given issue.

This is why I love declarative programming. It's hard to get used to at first, but once you are used to it, being able to directly inspect the thing that's wrong itself and see what's wrong with it—instead of some callback function or action handler that could literally be anywhere—really accelerates debugging.

Conclusion

For a full comparison, see the commit here.

NGXS is a very friendly library. The maintainers are extremely nice and helpful, and there’s a lot of material out there if you get stuck as well as a solid community of developers who can help you. And although the boilerplate is in the same ballpark as NgRx/Store, the philosophy of “progressive state management” means NGXS accommodates all of the imperative coding patterns you are familiar with, while also enabling higher levels of reactivity with RxJS as your skills and reactive habits develop.

StateAdapt is pretty much the complete opposite of NGXS. In fact, it was an NGXS project that first inspired it, and the original prototype was written in an NGXS state file. With a philosophy of “progressive reactivity”, the idea is that your code should always be as close to 100% declarative as possible, even if you have to bend your mind a little at first. But as you work at it, thinking reactively gets more and more as easy as thinking imperatively, and the benefits of reducing spaghetti code will pay massive dividends in the long run.

But StateAdapt is still a work in progress. I need to apply it to many more projects before I can have the confidence to release version 1.0. If you think it has potential, I'd appreciate a star, and I'd love for you to try it out and share your thoughts.

Thanks!


Repo

StateAdapt

NGXS

Twitter

Top comments (0)