Skip to content
Two virtual7 employees floating mid-air in a playful pose, one working on a laptop with the virtual7 logo.

Angular

External data: Centralized status management with Redux

Björn Möllers

Copy Link

Link copied

Redux

Another approach to holding and managing data is centralized state management with Redux. Redux helps us to avoid data inconsistency within our application and to make state changes transparent and traceable. The state of the application is stored in an unchangeable Store. State changes can be ordered via Actions. The Reducer processes the Actions and calculates the new state from the old state and the data from the Action, which is saved in the store. Reducer may not, however, have any other dependencies or perform asynchronous operations. Therefore, Reducer is not suitable for interacting with external interfaces – such as loading external data. Effects listens to the actions like a Reducer – but does not change the state. We can use a Effect to load the data. If successful, we trigger another Action (Load_Success) with the loaded data. A Reducer then writes the data to the Store. So much for the basics: let’s take a look at the program code!

Preparations

There are various frameworks for the use of Redux in Angular:

In this blog post we will use NgRx. For the installation, the following packages will be added to your Angular project:

npm install --save @ngrx/store @ngrx/effects

Aim of the application: We want to load a list of blog posts from an interface. To do this, we create an entity class that will hold our data for a blog post. It is the same entity class as we already used in the Loading external data via HTTP client part.

export class PostEntity {
    userId: number;
    id: number;
    title: string;
    body: string;

    static fromJson(obj: object): PostEntity {
        const newEntity = new PostEntity();

        if (obj.hasOwnProperty('userId')) {
            newEntity.userId = Number(obj['userId']);
        }

        if (obj.hasOwnProperty('id')) {
            newEntity.id = Number(obj['id']);
        }

        if (obj.hasOwnProperty('title')) {
            newEntity.title = obj['title'];
        }

        if (obj.hasOwnProperty('body')) {
            newEntity.body = obj['body'];
        }

        return newEntity;
    }
}

Status, actions and reducers

Let’s first define what we want to store in our state. The state is not global across all objects that we ever want to use, but we can define it separately for each subdomain. In addition to the list of blog posts list: PostEntity[], we also want to know whether the list has already been loaded (initLoad). Initially, we assume that the list is empty and still needs to be loaded.

import {PostEntity} from './post.entity';

export class PostState {
    initLoad = false;
    list: PostEntity[] = [];
}

To load the data, we define three Actions:

  • Load: Please load data
  • LoadSuccess: Loading was successful and the data is ready for the Store
  • LoadFailLoading was faulty
import {Action} from '@ngrx/store';
import {PostEntity} from './post.entity';

export enum PostActionTypes {
    Load = '[Post] Load',
    LoadSuccess = '[Post] Load Success',
    LoadFail = '[Post] Load Fail',
}

export class PostLoadAction implements Action {
    readonly type = PostActionTypes.Load;
}

export class PostLoadSuccessAction implements Action {
    readonly type = PostActionTypes.LoadSuccess;

    constructor(public posts: PostEntity[]) {

    }
}

export class PostLoadFailAction implements Action {
    readonly type = PostActionTypes.LoadFail;
}

The Reducer calculates the new status of the Stores from the Action and the old status.

  • The status is not changed at Load and LoadFail.
  • At LoadFail, an error message is displayed via the console – without changing the status.
    To simplify matters, we have omitted an error message to the user at this point.
  • At LoadSuccess the loaded data is saved in the list
    and the marker initLoad = true is set.

The JavaScript syntax const newState = {...state}; is important here: we never change the state directly, but work on your copy. The presented notation {...state} – also called Object Spread Operator – is a short form of Object.assign({}, state).

export const initialState: PostState = new PostState();

export function postReducer(state = initialState, action: Action): PostState {
    switch (action.type) {

        case PostActionTypes.LoadSuccess: {
            const newState = {...state};
            newState.list = (action as PostLoadSuccessAction).posts;
            newState.initLoad = true;
            return newState;
        }
        case PostActionTypes.LoadFail:
        {
            console.error({error: action});
            return state;
        }

        default:
            return state;
    }
}

Now we bring everything together: The AppModule still needs an import for StoreModule.forRoot({post: postReducer}). The configuration would work – but the data would never be loaded. We will therefore take a look at Effect in the next section. In AppModule we have added the necessary import EffectsModule.forRoot([ PostLoadEffect]).

@NgModule({
    declarations: [
        AppComponent,
        CreateComponent,
        ListComponent
    ],
    imports: [
        BrowserModule,
        HttpClientModule,
        FormsModule,
        StoreModule.forRoot({post: postReducer}),
        EffectsModule.forRoot([
            PostLoadEffect,
        ])
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule {
}

Effect: Load data from the backend

The effect loads the data from an API. Let’s take a look at the details: All Actions notifications are included.
However, as we are only interested in a specific type of action, we filter the Action notifications with ofType(PostActionTypes.Load). In the following program code, we will now only look at the actions PostActionTypes.Load that should load the backend data. We use this.http.get('https://jsonplaceholder.typicode.com/posts') to retrieve the desired data from the interface. In the first step, we transform it into the desired PostEntity class. In the second step, we build a PostLoadSuccessAction with the desired data. In the event of an error, we want to execute the PostLoadFailAction that we define with catchError(() => of(new PostLoadFailAction())). With flatMap or mergeMap we merge the second asynchronous event stream from this.http.get(...) (see also http://reactivex.io/documentation/operators/flatmap.html).

@Injectable()
export class PostLoadEffect {

    // Hört auf die Aktion 'LOAD'
    @Effect()
    load$: Observable = this.action$.pipe(
        ofType(PostActionTypes.Load),
        mergeMap(action =>
            this.http.get('https://jsonplaceholder.typicode.com/posts').pipe(
                map((list: object[]) => list.map((e: object) => PostEntity.fromJson(e))),
                map((list: PostEntity[]) => new PostLoadSuccessAction(list)),
                catchError(() => of(new PostLoadFailAction()))
            )
        )
    );

    constructor(private http: HttpClient, private action$: Actions) {

    }
}

Now we have almost everything: Redux is set up. Two things are still missing: Firstly, the action Load needs to be triggered and secondly, we still need to read the data from the store.

Trigger load action and display results

The Load-action in our example when creating the component (ngOnInit). We can read the Store with the help of store.pipe(select('post'));. We get the reference private store: Store<{ post: PostState }> using the dependency injection of Angular.

@Component({
    selector: 'app-list',
    templateUrl: './list.component.html',
    styleUrls: ['./list.component.css']
})
export class ListComponent implements OnInit {
    postState$: Observable;

    constructor(private store: Store<{ post: PostState }>) {
        this.postState$ = store.pipe(select('post'));
    }

    ngOnInit() {
        this.store.dispatch(new PostLoadAction());
    }

    onDelete(post: PostEntity) {
        this.store.dispatch(new PostDeleteAction(post));
    }

}

We use the async pipe to display the postState$ observable.

The code example goes even further and shows how data records can be added and deleted. You can find the program code at https://github.com/dornsebastian/angular-ngrx. In the next part, we will look at how we can upload and download files.


Angular
External data
Redux