Skip to content

MichaelKaaden/redux-client-ngrx

Repository files navigation

Angular NgRx Client

GitHub last commit GitHub tag GitHub version dependencies devDependencies GitHub issues license

This is a single-page application written in TypeScript using Angular 9. It retrieves counters from a REST service and displays them. You can increase and decrease each counter's value. A second page displays a little dashboard that does some analytics on your counters and their values.

Screenshot of the app running in the Browser

Purpose

I wanted to learn more about the Redux architectural pattern to solve common problems like the same data being used in multiple components. Changes in one component would not update the other component's data, so one would either have to deal with this chaos or notify the other components about change with e. g. events. This is complicated, error-prone and very ugly. Redux perfectly solves this problem. So I wrote this app together with the server side which you can find at the end of this document.

What do you think of @ngrx/entity?

Well... @ngrx/entity makes it very easy to write reducers, and its performance is very good. But it comes with two limitations you should consider:

  • Unit-testing becomes more tedious. You no longer can simply put your business objects inside your store. You have to create the matching ids: [...], entities: {...} instead. I made a little helper function for that:

    export function initializeStateWith(counters: Counter[]): CountersState {
        const state: CountersState = adapter.getInitialState();
        const ids: number[] = [];
        const entities: Dictionary<Counter> = {};
    
        for (const counter of counters) {
            ids.push(counter.index);
            entities[counter.index] = counter;
        }
    
        state.ids = ids;
        state.entities = entities;
    
        return state;
    }
  • The second thing that hurts much more is: You cannot use classes as base for the things you put into the store. You have to use plain objects. That makes sense because of serialization, but on the other hand it isn't fun initializing objects where a simple constructor would do.

    If you try to use classes, you'll stumble upon a problem: updateOne(...) as well as updateMany() copy properties from one object into a new one. Problem is: You're now no longer dealing with instances of classes, but simple objects.

    Let's look at a piece of code:

    export class Counter implements ICounter {
        public isLoading?: boolean;
        public isSaving?: boolean;
    
        constructor(public index: number, public value?: number) {}
    }
    
    const counter = new Counter(index, value);
    
    it("should return a counter out of the cache", () => {
        // prepare state to already have the counter loaded
        store.dispatch(new LoadPending({ index }));
        store.dispatch(new LoadCompleted({ index, counter }));
    
        const action = new LoadPending({ index });
        const completion = new LoadCompleted({ index, counter });
    
        const counterSpy = spyOn(counterService, "counter").and.returnValue(
            of(new Counter(index, value)),
        );
    
        actions$ = cold("--a-", { a: action });
        const expected = cold("--b", { b: completion });
    
        expect(effects.loadPending$).toBeObservable(expected);
        expect(counterSpy).not.toHaveBeenCalled();
    });

    The test will fail at the first expectation with the message "Expected $[0].notification.value.payload.counter to be a kind of Counter, but was Object({ index: 0, value: 42, isLoading: false })."

    As Mike Ryan says here, this behaviour is intentional.

Now it's your choice if you'll stick with implementing the state operations for yourself until this has changed or if you still want to use @ngrx/entity.

Unit Testing and Code Coverage

I tried to test as much as possible. The current code coverage is at almost 100%. The one thing missing is a test regarding the production environment which shouldn't load the storeFreeze meta reducer. I consider this a minor problem, though. ;-)

Code coverage

Running ng update

It seems ng update is not fully compatible with Yarn. See this issue.

To mitigate this, use the --from=x.x.x syntax. Example: ng update --from=6.2.5 @angular/cli.

Some hints on using @ngrx/schematics

First, generate a store using IAppState as state interface:

ng g store State --stateInterface IAppState --root --module app.module.ts

Then, create a set of actions, effects and reducers (called a feature) for the error component:

ng g feature error -m app.module.ts --group

Development server

Run ng serve for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.

Alternative and Corresponding Implementations

This is only one possible solution to this kind of problem.

There are some implementations of single-page applications using the services which are implemented in different programming languages and frameworks.

Here's the full picture.

Client-Side Implementations

Server-Side Implementations