DEV Community

Cover image for The "DeRxJSViewModel Pattern": The E=mc^2 of State Management [Part 2]
Zack DeRose
Zack DeRose

Posted on

The "DeRxJSViewModel Pattern": The E=mc^2 of State Management [Part 2]

👋 hi all - this article is a continuation of a previous article that explains the goals of the DeRxJS pattern:

  • entirely de-couple state management code from presentational code (to the point where your state management code could be re-used across frameworks)
  • maximize the benefits of RxJS, while minimizing the negatives
  • next-level testing [and potential to hand over all our state-management code to AI at some point]

In that article, we did most of the heavy lifting - developing our state-management code and fully testing out every edge case with "timeline testing" (allowing us to assert not only "what" state looks like, but "when" it should look that way as well).

In this article, we'll bring that state management code and show how we can use this across 3 front-end "frameworks": React, Angular, and Vanilla JS.

React

One of the goals of DeRxJS [as the name suggests] is to remove actual RxJS code from our code bases. We discussed in the previous example how @derxjs/reducer can help us write our state management, leveraging RxJS, but without actually writing any RxJS code ourselves.

@derxjs/react logo

In this article, I'm excited to introduce @derxjs/react - that will allow us to leverage that same approach to our react presentation code.

In all honesty, I've prioritized React as the first presentation-based package for derxjs in part because of React's popularity. But beyond that there are 2 huge reasons that I've targeted React first:

  1. RxJS and React don't really play well together [yet!]
  2. One of my favorite things about React is how it is not domain-specific, for nearly everything but state-management! (Interestingly, I think this is almost entirely inverted from Angular, which I'd argue is domain-specific for everything BUT state-management) Marrying React and RxJS I think can close that gap, so that the state-management code you write is as domain-agnostic as the rest of your react code.

In general, I'm quite long on React. My motivations are almost entirely selfish - I think if this package works the way I want it to, this will be my ideal environment for frontend development.

Without further ado, here's the general api for our @derxjs/react code:

export const TicTacToe = () => {
  return DeRxJSComponent<
    TicTacToeViewModelInputs,
    TicTacToeViewModel,
    TicTacToeProps
  >({
    viewModel$: ticTacToeViewModel$,
    component: TicTacToeView as any,
    initialValue: createInitialViewModel(),
    triggerMap: {
      spaceClick: 'userSpaceClickEvents$',
      resetClick: 'userResetClickEvents$',
    },
    inputs: {
      ai: randomAi,
    },
  });
};
Enter fullscreen mode Exit fullscreen mode
  • viewModel$ is imported from our work from the previous article
  • component is a presentational component (we'll see that next!)
  • initialValue is the starting value for our state (the createInitialViewModel() function comes from the previous article as well)
  • triggerMap is a [type-safe!] object that maps the name of "trigger" functions for our presentational components to Observable inputs of our viewModel$. "Trigger" functions are how we'll communicate the message passing our presentation component will need to perform, and hand this off to to the @derxjs/react package to turn those into Observables (so we don't have to write any of that RxJS code ourselves).
  • inputs is our way to provide any non-reactive (or non-Observable) inputs to our viewModel$ function. Note we're passing our randomAi function here - essentially parameterizing functionality of our viewModel$ this way. (Would be fun in future work to create an "unbeatable" ai as well!)

This API is designed to allow you to write all presentational code as "presentational" components, delegating any smarts to your @derxjs/view-model, and using the provided trigger functions for message passing.

Here's how that code ends up looking:

interface TicTacToeProps {
  spaceClick: (spaceCoordinates: SpaceCoordinates) => void;
  resetClick: () => void;
}

interface SpaceProps {
  contents: SpaceContent;
  spaceCoordinates: SpaceCoordinates;
  clickHandler: (spaceCoordinates: SpaceCoordinates) => void;
}
const Space = ({ contents, clickHandler, spaceCoordinates }: SpaceProps) => (
  <div>
    <button onClick={() => clickHandler(spaceCoordinates)}>
      {contents.toUpperCase()}
    </button>
  </div>
);

function TicTacToeView({
  state,
  triggers,
}: {
  state: TicTacToeViewModel;
  triggers: TicTacToeProps;
}) {
  return (
    <>
      <h2>{state.turn}</h2>
      <div className={'border'}>
        <div className={'board'}>
          {([0, 1, 2] as const)
            .map((row) => ([0, 1, 2] as const).map((column) => [row, column]))
            .flat()
            .map(([row, column]) => (
              <Space
                contents={state.board[row][column]}
                spaceCoordinates={{ row, column }}
                clickHandler={triggers.spaceClick}
                key={`${row}:${column}`}
              />
            ))}
        </div>
      </div>
      <button className="reset" onClick={triggers.resetClick}>
        Reset
      </button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note how onClicks are set to those "trigger functions" we defined.

Here's that code in action:

Angular

Next up: Angular! As mentioned, I'm of the opinion that Angular is generally very domain-agnostic when it comes to state management. In particular, it's very RxJS friendly.

As such, I don't know if a @derxjs/angular package is really necessary. Eventually we could end up creating a package of utilities for hiding more of the RxJS code that we'd write, but I have no plans for that at the moment.

Here's a look at the Typescript component code we'll write:

export class AppComponent {
  userResetClickObserver!: Observer<void>;
  userResetClickEvents$ = new Observable<void>(
    (observer) => (this.userResetClickObserver = observer)
  );
  userSpaceClickObserver!: Observer<SpaceCoordinates>;
  userSpaceClickEvents$ = new Observable<SpaceCoordinates>(
    (observer) => (this.userSpaceClickObserver = observer)
  );
  vm$ = ticTacToeViewModel$({
    ai: randomAi,
    userSpaceClickEvents$: this.userSpaceClickEvents$,
    userResetClickEvents$: this.userResetClickEvents$,
  });
  rows: BoardIndex[] = [0, 1, 2];

  handleSpaceClick(coordinates: SpaceCoordinates) {
    this.userSpaceClickObserver.next(coordinates);
  }

  handleResetClick() {
    this.userResetClickObserver.next();
  }
}
Enter fullscreen mode Exit fullscreen mode

Note how we're creating our observables by creating an 'observer' property on the class at "construction time", and then in our click handler methods, we call next() on those observers. (This essentially the same "message passing" as our React code, but the @derxjs/react package hid most of the actual code here)

Similar to our react example, we'll see the same idea of a 'presentational' component in our template - with the one exception of passing our viewModel$ to the Angular async pipe at the top level of our template:

<h1>Tic Tac Toe</h1>
<ng-container *ngIf="vm$ | async as vm">
  <h2>{{ vm.turn }}</h2>
  <div class="border">
    <div class="board">
      <ng-container *ngFor="let row of rows">
        <div *ngFor="let column of rows">
          <button (click)="handleSpaceClick({ row, column })">
            {{ vm.board[row][column] | uppercase }}
          </button>
        </div>
      </ng-container>
    </div>
  </div>
  <button class="reset" (click)="handleResetClick()">Reset</button>
</ng-container>
Enter fullscreen mode Exit fullscreen mode

Nice and simple :). Here's the stackblitz for our Angular code:

Vanilla JS

In this example, we'll use the dom-manipulation API to do the lifting that React and Angular were doing in their examples. Here's the simplified version of what we're doing:

  1. Create a "template" for our component, attaching id's to the buttons we'll need to listen to for clicks/update their text. This example is a bit fortunate as all the elements on the DOM are static (they don't need to be added or removed, so we can just leave the Element objects on the DOM as-is, and change their text content. This would be significantly more difficult if this were not the case).
  2. use fromEvent from RxJS to get observables of the 'click' events on the buttons.
  3. Once we have our observables, pass them to that same viewModel$ function we used in React and Angular to create our View Model observable.
  4. Subscribe to that observable, and update the 'board' by changing the text content of the buttons to match the board property on the view model object.

Here's how that looks in the stackblitz:

Conclusion

And that's a wrap!! Hope that this article helped spark some cool ideas. Be sure to reach out on twitter or check out the DeRxJS repo if you ever want to jam about state-management or good code architecture!!

About the author

Zack DeRose

Zack DeRose [or DeRxJS if you like] is:

Checkout out my personal website for more of my dev content! And go bug Jeff Cross/Joe Johnson if you want to hire me to come help out your codebase or come help level up your team on Nx/NgRx/DeRxJS/RxJS/State Management! (I especially love building awesome stuff - and building up teams with bright developers that are eager to learn!)

Top comments (0)