RuairĂ­'s Site

Where to put Angular with Redux

2018-03-05

I am reasonably new to Angular 2+ and have already made a bunch of bad decisions when using it with Redux. This is probably old news to most people but one thing I struggled with was how to work with Redux selectors and where to put them in components.

I have dabbled with React and Redux and one thing I liked a lot was the concept of functional components. That's where all the component state gets passed in as parameters and no internal modification of state is done. Therefore you can test any variation of the components behavior by modifying the inputs.

If state needs to be changed, the component emits events that produce actions that alter the application state. Any interaction with external things like the Redux store would happen in container components where you bind the inputs and outputs for any child components.

Good terms for these concepts that I came across in a post by Dan Abramov are Presentation and Container components. Presentation components being the simple components that take state as inputs and present it. Container components being the components that manage interaction with the likes of Redux and provide inputs to presentational components.

I recently started working on a reasonably large angular project. I struggled with finding good examples and developing a good idea of where things should go to keep the code base clean.

One thing that caught me out a lot was how coupled the component code was to Redux, or at least ng2-redux. This code base is making heavy use of the select pattern A typical component in the code base might look something like this, taking a really simple and made-up user list as an example.

@Component({ selector: 'app-users', templateUrl: './users.component.html' }) export class UsersComponent implements OnInit { @select(['userManagement', 'users']) users$: Observable<User[]>; constructor(private userManagementActions: UserManagementActions) { } @dispatch() ngOnInit() { return this.userManagementActions.fetchUsers(); } }

And a simple template for that.

<ul> <li *ngFor="let user of (users$ | async)">{{ user.name }}</li> </ul>

There's nothing particularly wrong with this. It will work fine.

From a design and clean code perspective, issues can start to crop up when this component is used in other components and if this component is going to get used in more that one place.

Let's look at how we might test this component.

describe("UsersComponent", () => { let component: UsersComponent let fixture: ComponentFixture<UsersComponent> let userManagementActions: UserManagementActions let element: HTMLElement let ngRedux let usersSelector beforeEach(async(() => { TestBed.configureTestingModule({ imports: [NgReduxTestingModule], declarations: [UsersComponent], providers: [UserManagementActions], }).compileComponents() })) beforeEach(() => { MockNgRedux.reset() usersSelector = MockNgRedux.getSelectorStub(["userManagement", "users"]) ngRedux = TestBed.get(NgRedux) spyOn(ngRedux, "dispatch") fixture = TestBed.createComponent(UsersComponent) userManagementActions = TestBed.get(UserManagementActions) component = fixture.componentInstance element = fixture.debugElement.nativeElement fixture.detectChanges() }) it("should dispatch fetchUsers action", () => { expect(ngRedux.dispatch).toHaveBeenCalledWith( userManagementActions.fetchUsers() ) }) it("should render users", () => { usersSelector.next([ { id: "one", name: "First" }, { id: "two", name: "Second" }, ]) fixture.detectChanges() const userElements = element.querySelectorAll("li") expect(userElements[0].textContent).toContain("First") expect(userElements[1].textContent).toContain("Second") }) })

It's a really simple component but there is still a fairly large amount of boilerplate test setup code there. Also, we have really embedded Redux in to that component. The test needs to do a lot of mocking. We need to mock Redux, verify we dispatch an action on initialization, mock what comes from the store and test that we render that correctly.

What if we need an almost identical component but want to dispatch a slightly different action on initialization?

Doing all this setup for every component test gets to be a bit of a drag.

There's just a bunch of issues with doing components this way I think but it still seems to be a common pattern.

The solution to avoid these issues may be really obvious to most but I had to struggle a bit before arriving at it.

Presentation Components

We don't have the functional syntax for components that React has in Angular but we have something similar.

Angular components have the @Input() and @Output() property decorators. These allow us to take all the required state as inputs and emit any internal events as outputs. If we emit an event to update the Redux store for example, the inputs can be changed in the container and the components can be re-rendered. This way the component only needs to know how to present the input state and what to emit on an interaction.

We could instead write the component like this.

@Component({ selector: "app-users", templateUrl: "./users.component.html", changeDetection: ChangeDetectionStrategy.OnPush, }) export class UsersComponent { @Input() users: User[] = [] }

The template would be updated to look like this (just removing the async pipe).

<ul> <li *ngFor="let user of users">{{ user.name }}</li> </ul>

The tests get a bit easier for that component then. Just set the value of the input add test the rendering.

describe("UsersComponent", () => { let component: UsersComponent let fixture: ComponentFixture<UsersComponent> let element: HTMLElement beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [UsersComponent], }).compileComponents() })) beforeEach(() => { fixture = TestBed.createComponent(UsersComponent) component = fixture.componentInstance element = fixture.debugElement.nativeElement }) it("should render users", () => { component.users = [ { id: "one", name: "First" }, { id: "two", name: "Second" }, ] fixture.detectChanges() const userElements = element.querySelectorAll("li") expect(userElements[0].textContent).toContain("First") expect(userElements[1].textContent).toContain("Second") }) })

There's a lot less boilerplate and I think the tests are a lot cleaner since we are not mocking or stubbing internal state.

Because we have kept the component so simple and with less dependencies it is easier to test and also much more reusable than if we used those Redux selectors directly.

changeDetection: ChangeDetectionStrategy.OnPush

You may have noticed we set the changeDetection property for the component. This is something we should try to use when writing our components this way. When we set this we are saying we will not change the inputs internally. This allows a little optimization because Angular won't check the internal state to see if it needs to update the rendering. Therefore if you change the state internally, the updates will not get picked up for rendering. The change detection strategy checks the reference of the input and if that has not changed, it does nothing. This makes it work well with Redux since we will always bring back a new reference from the store if there was a change.

Container Components

We can't avoid having dependencies between Redux and our components completely but we can keep it in container components.

Say you have a component representing a page that contains a list of users and maybe something else. That could be the Container component since it probably won't have any complicated things to do itself. For example:

<div> <some-other-thing></some-other-thing> <app-users [users]="users$ | async"></app-users> </div>
@Component({ selector: 'app-user-management', templateUrl: './user-management.component.html' }) export class UserManagementComponent implements OnInit { @select(['userManagement', 'users']) users$: Observable<User[]>; constructor(private userManagementActions: UserManagementActions) { } @dispatch() ngOnInit() { return this.userManagementActions.fetchUsers(); } }

Now we only need to test the Redux related things there (with all the mocking and boilerplate setup code), keeping the child components simple.

In React the tendency is to have one container component for each presenter with the container having no makeup of its own. This is another potential pattern to try out here.

Interactions

What if you need a component to dispatch and action to the store?

So lets say the user list has a delete button and on clicking it, you want to fire off an action to delete that user.

@dispatch() deleteUser(user: User) { return this.userManagementActions.deleteUser(user); }

You could add that to the UsersComponent but then you're back to adding all that Redux test stuff. Instead it might be good to emit that event using an Output.

@Output() onDeleteUser: EventEmitter<User> = new EventEmitter<User>(); deleteUser(user: User) { this.onDeleteUser.emit(user); }

Now we can dispatch the action in the container by listening for that event.

<div> <some-other-thing></some-other-thing> <app-users [users]="users$ | async" (onDeleteUser)="deleteUser($event)" ></app-users> </div>

Conclusion

I am definitely finding Angular code easier to understand and test using this technique. It's still not great but it's a bit more manageable. Angular and Redux are both fine tools but, like any code bases, the code base using them can start to get pretty messy unless some decent patterns are used. The Container & Presenter pattern is a good one for this scenario I believe.

Some runnable example code for the above is here: https://github.com/ruarfff/angular-demo-code/tree/master/src/app/user-management