Describe Your NgRx Actions More To Write Less Code
Tags: angular, redux
Published: 2021-03-06 05:31:18

Actions describe events in our NgRx-powered applications.

When we fail to sufficiently describe our actions, we get duplicative code. This results in higher maintenance cost and slower time to implement features.

Instead, we can define a structure for action metadata, and inject that into our actions when they are instantiated.

Then, we can more generically react to actions with that metadata while preserving good action hygiene and allowing actions to continue to operate for their more narrow purposes.

## An example

Consider the following action:

```typescript
export const LoadBillingAddressSuccess = createAction(
  '[Address API] Load Billing Address Success',
  props<{ address: Address }>()
); 
```

When an instance of this action is instantiated, it will be an object that looks something like

```typescript
{
  type: '[Address API] Load Billing Address Success',
  address: { /* some address */ }
}
```

Examining the object, we know that the action
- is a discrete event named `[Address API] Load Billing Address Success`
- occurred for a given `address`

With that information, we are able to write a useful state change in our reducer:

```typescript
on(LoadBillingAddressSuccess, (state, { address }) => ({
  ...state,
  billingAddress: address
}))
```

### The feature requirements

Say we were given two requirements for displaying this billing address on a webpage:

- While the billing address is loading, show a loading indicator panel
- When the billing address fails to load, show a failure toast notification

### Surveying a possible implementation

For the loading indicator panel, it would make sense to have some kind of "request state" we can track.

Depending on if the request state is in progress or has completed, we can display either the loading indicator or the address component.

When we go to implement this, however, we find that this idea has already been implemented for another request state:

```typescript
// in the reducer
on(LoadHomeAddressStarted, state => ({
  ...state,
  loadHomeAddressRequestState: 'started'
})),
on(LoadHomeAddressSuccessful, state => ({
  ...state,
  loadHomeAddressRequestState: 'successful'
}))

// in the selectors
export const selectLoadHomeAddressRequestState = createSelector(
  selectAddressesState,
  state => state.loadHomeAddressRequestState
);

export const selectLoadHomeAddressRequestStateIsInProgress = createSelector(
  selectLoadHomeAddressRequestState,
  requestState => requestState === 'started'
);
```

Similarly, for the failure toast notification, we find that an effect already exists for the "home address" as well:

```typescript
showLoadHomeAddressFailedNotification$ = createEffect(() =>
  this.actions$.pipe(
    ofType(LoadHomeAddressFailed),
    tap(() => this.notificationService.push('Failed to load Home Address', 'failure'))
  ),
  { dispatch: false }
);
```

### Dealing with common requirements

While the billing address and home address-related actions are all distinct, they seem to be related by having common resulting behavior.

Without breaking good action hygiene, we can better describe our actions to easily react to them in a more generic way.

#### Describing actions as request state milestones

We can define a request state and describe actions as a milestone for a stage of that request state.

Without worrying about the internal details, say I have a function like `createRequestState` that operates like so:

```typescript
export const LoadBillingAddressRequestState = createRequestState();

LoadBillingAddressRequestState.aSuccess();
// produces an object like
/*
  {
    requestStateMetadata: {
      uuid: 'some-uuid',
      milestone: 'success'
    }
  }
*/
```

Then by using the "creator" API of `createAction`, we can inject this metadata into the payload of our action:

```typescript
export const LoadBillingAddressSuccess = createAction(
  '[Address API] Load Billing Address Success',
  (properties: { address: Address }) => ({
    ...properties,
    ...LoadBillingAddressRequestState.aSuccess()
  })
); 
```

The action is still instantiated the same way, but now produces an object like this:

```typescript
LoadBillingAddressSuccess({ address: someBillingAddress })

/* produces
  { 
    type: '[Address API] Load Billing Address Success',
    address: someBillingAddress,
    requestStateMetadata: {
      uuid: 'some-uuid',
      milestone: 'success'
    }
  }
*/
```

Now that we have `requestStateMetadata` on the action, we can react to it in a more generic way:

```typescript
// in new request-state.effects.ts
mapToRequestStateChanged$ = createEffect(() =>
  this.actions$.pipe(
    filter(action => !!action['requestStateMetadata']),
    map(action => RequestStateChanged(action['requestStateMetadata']))
  )
);

// in new request-state.reducer.ts
on(RequestStateChanged, (state, { uuid, milestone }) => ({
  ...state,
  [uuid]: milestone
)}) 
```

Our existing reducer code to update the billing address in the address reducer still works just fine! But now we're also progressing the state for this request in a way that's easy to read straight from the action declaration.

As a bonus, we could implement a selector within the object our magic `createRequestState` function produces such that we can easily select if the request state is in progress:

```typescript
LoadBillingAddressRequestState.selectIsInProgress();

/* produces a selector like
  createSelector(
    selectRequestState(uuid),
    requestState => requestState === 'started'
  );
*/
```

#### Describing actions as notifiable failures

Implementing a similar metadata approach for notifications is simple.

We can declare a function that operates like so:

```typescript
aNotifiableFailure('A failure message.');
// produces an object like
/*
  {
    failureNotificationMetadata: {
      message: 'A failure message.'
    }
  }
*/
```

Again, we can describe our action with the `aNotifiableFailure` metadata creator.
Interestingly, if we want our failure message to be dynamic based on a property from the action, we can do that!

```typescript
export const LoadBillingAddressFailure = createAction(
  '[Address API] Load Billing Address Failure',
  (properties: { serverError: ServerError }) => ({
    ...properties,
    ...aNotifiableFailure(serverError.message)
  })
); 
```

The action creator will work like so:

```typescript
LoadBillingAddressFailure({ serverError: someServerError })

/* produces
  { 
    type: '[Address API] Load Billing Address Failure',
    serverError: someServerError,
    failureNotificationMetadata: {
      message: someServerError.message
    }
  }
*/
```

And now all failures can be handled in one effect:

```typescript
// in new notifications.effects.ts
showFailureNotification$ = createEffect(() =>
  this.actions$.pipe(
    filter(action => !!action['failureNotificationMetadata']),
    tap(() => this.notificationService.push(action['failureNotificationMetadata'].message, 'failure'))
  ),
  { dispatch: false }
);
```

## Handling descriptive actions reduces code

By injecting metadata into our actions, we reduce the amount of code we have to write for handling similar behavior across our application while maintaining good action hygiene.

The pattern also increases the usefulness of the actions file by giving a reader a more complete picture of what an action represents.