Intermediate Redux in Angular

In the last chapter we learned about Redux, the popular and elegant data architecture. In that chapter, we built an extremely basic app that tied our Angular components and the Redux store together.

In this chapter we’re going to take on those ideas and build on them to create a more sophisticated chat app.

Here’s a screenshot of the app we’re going to build:

logo

Context For This Chapter

Earlier in this book we built a chat app using RxJS. We’re going to be building that same app again only this time with Redux. The point is for you to be able to compare and contrast how the same app works with different data architecture strategies.

You are not required to have read the RxJS chapter in order to work through this one. This chapter stands on its own with regard to the RxJS chapters. If you have read that chapter, you’ll be able to skim through some of the sections here where the code is largely the same (for instance, the data models themselves don’t change much).

We do expect that you’ve read through the previous Redux chapter or at least have some familiarity with Redux.

Chat App Overview

In this application we’ve provided a few bots you can chat with. Open up the code and try it out:

{lang=shell,line-numbers=off}
cd code/redux/redux-chat npm install npm start

Now open your browser to http://localhost:4200.

Notice a few things about this application:

Let’s look at an overview of how this app is constructed. We have

Let’s look at them one at a time.

Components

The page is broken down into three top-level components:

logo

Models

This application also has three models:

logo

Reducers

In this app, we have two reducers:

Summary

At a high level our data architecture looks like this:

In the rest of this chapter, we’re going to go in-depth on how we implement this using Angular and Redux. We’ll start by implementing our models, then look at how we create our app state and reducers, and then finally we’ll implement the Components.

Implementing the Models

Let’s start with the easy stuff and take a look at the models.

We’re going to be specifying each of our model definitions as interfaces. This isn’t a requirement and you’re free to use more elaborate objects if you wish. That said, objects with methods that mutate their internal state can break the functional model that we’re striving for.

That is, all mutations to our app state should only be made by the reducers - the objects in the state should be immutable themselves.

So by defining an interface for our models,

  1. we’re able to ensure that the objects we’re working with conform to an expected format at compile time and
  2. we don’t run the risk of someone accidentally adding a method to the model object that would work in an unexpected way.

User

Our User interface has an id, name, and avatarSrc.

    /**
     * A User represents an agent that sends messages
     */
    export interface User {
      id: string;
      name: string;
      avatarSrc: string;
      isClient?: boolean;
    }

We also have a boolean isClient (the question mark indicates that this field is optional). We will set this value to true for the User that represents the client, the person using the app.

Thread

Similarly, Thread is also a TypeScript interface:

    import { Message } from '../message/message.model';
    
    /**
     * Thread represents a group of Users exchanging Messages
     */
    export interface Thread {
      id: string;
      name: string;
      avatarSrc: string;
      messages: Message[];
    }

We store the id of the Thread, the name, and the current avatarSrc. We also expect an array of Messages in the messages field.

Message

Message is our third and final model interface:

    import { User } from '../user/user.model';
    import { Thread } from '../thread/thread.model';
    
    /**
     * Message represents one message being sent in a Thread
     */
    export interface Message {
      id?: string;
      sentAt?: Date;
      isRead?: boolean;
      thread?: Thread;
      author: User;
      text: string;
    }

Each message has:

App State

Now that we have our models, let’s talk about the shape of our central state. In the previous chapter, our central state was a single object with the key counter which had the value of a number. This app, however, is more complicated.

Here’s the first part of our app state:

    export interface AppState {
     users: UsersState;
     threads: ThreadsState;
    }

Our AppState is also an interface and it has two top level keys: users and threads - these are defined by two more interfaces UsersState and ThreadsState, which are defined in their respective reducers.

A Word on Code Layout

This is a common pattern we use in Redux apps: the top level state has a top-level key for each reducer. In our app we’re going to keep this top-level reducer in app.reducer.ts.

Each reducer will have it’s own file. In that file we’ll store:

The reason we keep all of these different things together is because they all deal with the structure of this branch of the state tree. By putting these things in the same file it’s very easy to refactor everything at the same time.

You’re free to have multiple layers of nesting, if you so desire. It’s a nice way to break up large modules in your app.

The Root Reducer

Since we’re talking about how to split up reducers, let’s look at our root reducer now:

    export interface AppState {
     users: UsersState;
     threads: ThreadsState;
    }
    
    const rootReducer: Reducer<AppState> = combineReducers<AppState>({
     users: UsersReducer,
     threads: ThreadsReducer
    });
    
    export default rootReducer;

Notice the symmetry here - our UsersReducer will operate on the users key, which is of type UsersState and our ThreadsReducer will operate on the threads key, which is of type ThreadsState.

This is made possible by the combineReducers function which takes a map of keys and reducers and returns a new reducer that operates appropriately on those keys.

Of course we haven’t finished looking at the structure of our AppState yet, so let’s do that now.

The UsersState

Our UsersState holds a reference to the currentUser.

    export interface UsersState {
      currentUser: User;
    };
    
    const initialState: UsersState = {
      currentUser: null
    };

You could imagine that this branch of the state tree could hold information about all of the users, when they were last seen, their idle time, etc. But for now this will suffice.

We’ll use initialState in our reducer when we define it below, but for now we’re just going to set the current user to null.

The ThreadsState

Let’s look at the ThreadsState:

    export interface ThreadsEntities {
      [id: string]: Thread;
    }
    
    export interface ThreadsState {
      ids: string[];
      entities: ThreadsEntities;
      currentThreadId?: string;
    };
    
    const initialState: ThreadsState = {
      ids: [],
      currentThreadId: null,
      entities: {}
    };

We start by defining an interface called ThreadsEntities which is a map of thread ids to Threads. The idea is that we’ll be able to look up any thread by id in this map.

In the ThreadsState we’re also storing an array of the ids. This will store the list of possible ids that we might find in entities.

This strategy is used by the commonly-used library normalizr. The idea is that when we standardize how we store entities in our Redux state, we’re able to build helper libraries and it’s clearer to work with. Instead of wondering what the format is for each tree of the state, when we use normalizr a lot of the choices have been made for us and we’re able to work more quickly.

I’ve opted not to teach normalizr in this chapter because we’re learning so many other things. That said, I would be very likely to use normalizr in my production applications.

That said, normalizr is totally optional - nothing major changes in our app by not using it.

If you’d like to learn how to use normalizr, checkout the official docs, this blog post, and the thread referenced by Redux creator Dan Abramov here

 
This page is a preview of ng-book 2.
Get the rest of this chapter plus hundreds of pages Angular 7 instruction, 5 sample projects, a screencast, and more.

 

Ready to master Angular 7?

  • What if you could master the entire framework – with solid foundations – in less time without beating your head against a wall? Imagine how quickly you could work if you knew the best practices and the best tools?
  • Stop wasting your time searching and have everything you need to be productive in one, well-organized place, with complete examples to get your project up without needing to resort to endless hours of research.
  • You will learn what you need to know to work professionally with ng-book: The Complete Book on Angular 7 or get your money back.
Download the First Chapter (for free)