DEV Community

Cover image for 31% less code: RxAngular vs StateAdapt 1. Todo MVC
Mike Pearson for This is Angular

Posted on • Updated on

31% less code: RxAngular vs StateAdapt 1. Todo MVC

YouTube video for this article

I implemented Todo MVC with StateAdapt and the code decreased by 31%.

Todo MVC UI

State change logic

The main difference is how state change logic is defined in StateAdapt. Some may not like how compact it is. However, the compactness enables portability, so I like it. Here it is in StateAdapt:

  todosAdapter = createAdapter<Todo[]>()({
    create: (todos, text: Todo['text']) => [
      ...todos,
      {
        id: Math.round(Math.random() * 100000),
        text,
        done: false,
      },
    ],
    remove: (todos, { id }: Todo) => todos.filter((todo) => todo.id !== id),
    update: (todos, { id, text, done }: Todo) =>
      todos.map((todo) => (todo.id !== id ? todo : { id, text, done })),
    toggleAll: (todos, done: Todo['done']) =>
      todos.map((todo) => ({ ...todo, done })),
    clearCompleted: (todos) => todos.filter(({ done }) => !done),
    selectors: {
      completed: (todos) => todos.filter(({ done }) => done),
      active: (todos) => todos.filter(({ done }) => !done),
    },
  });
Enter fullscreen mode Exit fullscreen mode

In StateAdapt, you can define state management patterns for each type/interface, and combine adapters to manage the combined type/interface. Like this:

  adapter = joinAdapters<TodoState>()({
    filter: createAdapter<TodoFilter>()({ selectors: {} }),
    todos: this.todosAdapter,
  })({
    /**
     * Derived state
     */
    filteredTodos: (s) =>
      s.todos.filter(({ done }) => {
        if (s.filter === 'all') return true;
        if (s.filter === 'active') return !done;
        if (s.filter === 'completed') return done;
      }),
  })();
Enter fullscreen mode Exit fullscreen mode

This takes the state changes and selectors for each child adapter and makes it available on the new parent adapter, by using TypeScript magic to prepend the names by its property name (filter or todos).

This can create some naming awkwardness for selectors. The todosAdapter's selector completed becomes available on the new adapter as adapter.todosCompleted, whereas natural English would have called it adapter.completedTodos. However, since this drills down from general to specific, there is something nice about it too. In any case, it's not a problem.

More naming awkwardness can come from state change names. For now. Because I am working on a potential solution for this. But the issue is with plural vs singular. Since state change names are verbs, I put the first word first, then the namespace the state change came from, then the rest of the state change name. So set on a child adapter becomes setFilter, for example. setToTrue would become setCheckedToTrue if the namespace was checked. But in this example, we have create, as in, create a single todo item. This becomes adapter.createTodos, even though it only creates one. But in the future I plan on creating a list adapter, so this would become addOne, similar to NgRx/Entity (which was a primary inspiration for StateAdapt in the first place).

Anyway, despite this awkwardness, it allows you to easily define compact, reusable state logic.

Here is the same code in RxAngular:

interface Commands {
  create: Pick<Todo, 'text'>;
  remove: Pick<Todo, 'id'>;
  update: Pick<Todo, 'id' | 'text' | 'done'>;
  toggleAll: Pick<Todo, 'done'>;
  clearCompleted: Pick<Todo, 'done'>;
  setFilter: TodoFilter;
}

// ...
  /**
   * UI actions
   */
  private readonly commands = this.factory.create();

  /**
   * State
   */
  private readonly _filter$ = this.select('filter');
  private readonly _allTodos$ = this.select('todos');

  /**
   * Derived state
   */
  private readonly _filteredTodos$ = this.select(
    selectSlice(['filter', 'todos'])
  ).pipe(
    map(({ todos, filter }) =>
      todos.filter(({ done }) => {
        if (filter === 'all') return true;
        if (filter === 'active') return !done;
        if (filter === 'completed') return done;
      })
    )
  );
  private readonly _completedTodos$ = this._allTodos$.pipe(
    map((todos) => todos.filter((todo) => todo.done))
  );
  private readonly _activeTodos$ = this._allTodos$.pipe(
    map((todos) => todos.filter((todo) => !todo.done))
  );

// ...
    /**
     * State handlers
     */
    this.connect('filter', this.commands.setFilter$);
    this.connect('todos', this.commands.create$, ({ todos }, { text }) =>
      insert(todos, {
        id: Math.round(Math.random() * 100000),
        text,
        done: false,
      })
    );
    this.connect('todos', this.commands.remove$, ({ todos }, { id }) =>
      remove(todos, { id }, 'id')
    );
    this.connect(
      'todos',
      this.commands.update$,
      ({ todos }, { id, text, done }) => update(todos, { id, text, done }, 'id')
    );
    this.connect('todos', this.commands.toggleAll$, ({ todos }, { done }) =>
      update(todos, { done }, () => true)
    );
    this.connect(
      'todos',
      this.commands.clearCompleted$,
      ({ todos }, { done }) => remove(todos, { done }, 'done')
    );
// ...
Enter fullscreen mode Exit fullscreen mode

RxAngular is awesome because it puts RxJS first, unlike most other state management libraries. The ability to have in your state/store's declaration that it will react to an observable can really make state management cleaner.

RxAngular is awesome.

Callbacks

Another big difference is that I eliminated callback functions. RxAngular's implementation has this:

  setFilter(filter: TodoFilter): void {
    this.commands.setFilter(filter);
  }

  create(todo: Pick<Todo, 'text'>): void {
    this.commands.create(todo);
  }

  remove(todo: Pick<Todo, 'id'>): void {
    this.commands.remove(todo);
  }

  update(todo: Todo): void {
    this.commands.update(todo);
  }

  toggleAll(todo: Pick<Todo, 'done'>): void {
    this.commands.toggleAll(todo);
  }

  clearCompleted(): void {
    this.commands.clearCompleted({ done: true });
  }
Enter fullscreen mode Exit fullscreen mode

Here's what I changed that to in StateAdapt:

  setFilter = this.store.setFilter;
  create = this.store.createTodos;
  remove = this.store.removeTodos;
  update = this.store.updateTodos;
  toggleAll = this.store.toggleTodosAll;
  clearCompleted = this.store.clearTodosCompleted;
Enter fullscreen mode Exit fullscreen mode

Although it could have been similar in the RxAngular implementation, I decided not to do it this way, because StateAdapt is extremely committed to completely reactive code organization. Traditionally, callback functions like these were seen as a good practice because they allow future flexibility. But they only allow for imperative flexibility. Observables are already flexible, because anything can refer to them and be updated, as long as there is a declarative API for it. Read more about this philosophy here.

This extreme commitment can make StateAdapt awkward to use when you're dealing with imperative APIs everywhere. But the solution is to create declarative wrapper for those APIs.

Conclusion

For a full comparison, see this PR comparison (but please don't open it).

RxAngular is still currently my top pick for state management in Angular. 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!

Links

Repo

StateAdapt

RxAngular

Twitter

Oldest comments (2)

Collapse
 
pterpmnta profile image
Pedro Pimienta M.

Great, Is this a alternative to Redux in Angular?

Collapse
 
mfp22 profile image
Mike Pearson

Yes. It accomplishes the same unidirecionality, but with much less boilerplate. medium.com/weekly-webtips/introduc...