2
votes

Let's say I have a few actions that my store can use: Load, LoadFail, and LoadSuccess. All my actions are very simple, LOAD = "load", LOAD_FAIL = "load failed", and LOAD_SUCCESS = "load success" I have a simple reducer that switches on these, like so:

export function reducer(state: State = initialState, action: Actions): State {
    switch (action.type) {
        case Actions.LOAD: {
            console.log("Actions.LOAD");
            return state;
        }
        case Actions.LOAD_FAIL: {
            console.log("Actions.LOAD_FAIL");
            store.error = action.payload;
            return state;
        }
        case Actions.LOAD_SUCCESS: {
            console.log("Actions.LOAD_SUCCESS");
            state.data = action.payload;
            return state;
        }
        default: {
            return state;
        }
    }
}

I have an effects class that handles the load dispatch:

@Injectable()
export class TestersEffects {
    constructor(
        private service: MyHttpService,
        private actions$: Actions
    ) {}

    @Effect() load$ = this.actions$.pipe(
        ofType(Actions.LOAD),
        switchMap(
            action => {
                return this.service.load().pipe(
                    map(data => {
                        console.log("load$ fired");
                        return new TestersActions.LoadSuccess(data);
                    }),
                    catchError(error => {
                        console.log("error");
                        return of (new Actions.LoadFail(error));
                    })
                )
            }
        )
    );
}

Finally, I'm using all of this as such:

export class MyClass implements OnInit {
    data: any;

    constructor(private store: Store<AppState>) {}

    ngOnInit() {
        this.store.dispatch(new Actions.Load());
        this.store.select(store => store.State).subscribe(
            data => this.data = data,
            error => this.handleError(error)
        );
    }

    handleError(error) {
        //Whatever I'll do to handle this error
    }
}

This code works fine when the server I'm requesting responds. However, I need to handle the situations in which it doesn't respond. I'm assuming that I simply don't understand the flow of ngrx/store well enough to fully comprehend how to get my errors, but I'm at a loss. When I debug my code, the catchError in my effects class fires and sends off a LoadFail action, the reducer catches the action, sets the error and returns the state. However, on my subscription side of things in MyClass, I never see anything. I figure I must be missing something critical here, but all my Googling expertise has left me empty handed and I figured I'd brave Stack Overflow and beg for its wisdom on the matter.

tl;dr: My store works just fine when the server responds, but when it doesn't, my errors aren't being sent to the subscription of a store state observable.

2

2 Answers

0
votes

I think the problem you're having here is it looks like you're trying to push the error down through your store as an error in the observable chain rather than making the error another part of your state.

What you're looking for is something more like:

// Add a property to your state object to hold errors...
interface State {
  // ...
  error: Error | null;
}

// Then in your reducer...
case Actions.LOAD_FAIL: {
  console.log("Actions.LOAD_FAIL");
  return { ...state, error: action.error };
}

Then in your subscription interrogating the value of this error field in the store will let you know if request failed and any error dispatched (I'd recommend making a selector for this that you can subscribe to directly).

Also I've not tried it myself but I have a feeling that attempting to error the store observable in this way will kill the store in your application so shouldn't really be attempted.

0
votes

I figured it out. I'm going to put it in analogy and then explain it technically.

Let's say you have a certain car, with its own unique VIN, like all cars do. In that car, you have a several seats that people can sit in. When the car arrives, you only really care about the people that are inside of it, and not necessarily the car itself. Essentially, what's happening in my original example is that it was as if I was checking to see if it was a new car, and not new people. So in order to make the system work, I had to check the people, and not the car.

So the technical of it. When you compare two objects, if their references are not the same, then they are not the same object, regardless of the data they contain. In my case, I was wanting to observe a change on my state object. The reason this didn't work is because the state object's reference never changed, despite the inner data changing. So how did I fix it? Well, there are two ways of going about it. If you want to continue observing changes in the state object itself, you need to reassign it in the reducer with the new data you want to give. Otherwise, if you only want to observe changes/errors, then you need to observe each variable you care about observing.

Essentially, it boils down to how this equates:

interface Person {
    name: string;
    id: number;
}

function comparePeople() {
    var p1: Person = { name: "John", id: 1 }
    var p2: Person = { name: "James", id: 2 }
    var p3: Person = { name: "John", id: 1 }

    console.log(`p1 === p1 ? ${p1 === p1});
    console.log(`p1 === p2 ? ${p1 === p2});
    console.log(`p1 === p3 ? ${p1 === p3});
}