Skip to content

Optimistic state updates using @nrwl/angular#optimisticUpdate

Notifications You must be signed in to change notification settings

mrkpks/nx-optimistic-state

Repository files navigation

NxOptimisticState

Quickstart:

$ npm ci

$ npx nx serve ou-ui

This simple project was created as a PoC to show comparison of two different approaches to handling API calls using NgRx Store and @nrwl/angular.

Sources overview

check these files for more info:

Classic approach

Using loading progress in views triggered by loading flags on FE.

This pattern is relying on 3 types of NgRx Store Actions:

  • Trigger Action (create, update/edit, delete) which triggers API call; e.g. deleteTask
  • Success Action which processes the response from API; e.g. deleteTaskSuccess
  • Failure Action which processes the error response from API; e.g. deleteTaskFailure
Classic success Classic error
image image

Examples

Classic delete

import * as TasksActions from './tasks.actions';

...
  deleteTask$ = createEffect(() =>
    this.actions$.pipe(
      ofType(TasksActions.deleteTask),
      fetch({
        run: ({ id }) =>
          this.fakeAPI
            .deleteTask(id)
            .pipe(map((id) => TasksActions.deleteTaskSuccess({ id }))),
        onError: (action, error) => {
          console.error('Error deleteTask$: ', error);
          this.message.error('Could not delete task');
          return TasksActions.deleteTaskFailure({ error });
        },
      })
    )
  );
Classic success Classic error
delete-classic-scss delete-classic-err

Classic Create task implementation

import * as TasksActions from './tasks.actions';

...
  createTask$ = createEffect(() =>
    this.actions$.pipe(
      ofType(TasksActions.createTask),
      fetch({
        run: (action) =>
          this.fakeAPI
            .createTask(action.name, action.status)
            .pipe(map((task) => TasksActions.createTaskSuccess({ task }))),
        onError: (action, error) => {
          console.error('Error createTask$: ', error);
          this.message.error('Could not create task');
          return TasksActions.createTaskFailure({ error });
        },
      })
    )
  );
Classic success Classic error
clsc-scss clsc-err

Optimistic approach

The Optimistic UI is ideal when the responses from API take longer than desired from UX point of view. This approach assumes a high success rate of received API responses and (in ideal case*) expecting the response from API wouldn't differ from the actual data modified on client.

Most times is relying on 2 types of NgRx Store Actions:

  • Trigger Action (create, update/edit, delete) which triggers API call AND immediately sets the new state containing new data on client (in store); e.g. deleteTaskOptimistic
  • Undo Action which is triggered upon receiving an error from API. Its purpose is to revert the new state to original state before the data modification.
Optimistic success Optimistic error
image image

Examples

Optimistic delete

import * as TasksActions from './tasks.actions';

...
  deleteTaskOptimistic$ = createEffect(() =>
    this.actions$.pipe(
      ofType(TasksActions.deleteTaskOptimistic),
      optimisticUpdate({
        run: ({ task }) =>
          this.fakeAPI.deleteTask(task.id).pipe(switchMap(() => EMPTY)),
        undoAction: ({ task }, error) => {
          console.error('Error deleteTask$: ', error);
          this.message.error('Could not delete task');
          return TasksActions.undoDeleteTask({
            error,
            task,
          });
        },
      })
    )
  );
Optimistic success Optimistic error
delete-opt-scss delete-opt-err

Edge case for optimistic approach

In some cases, just as in optimistic create, there can be a requirement for optimistic approach even though the API response differs in some way from the data modified on client. This use-case can be handled by adding a Success Action which re-maps or adds missing data to our entity.

Optimistic success Optimistic error
image image

Examples

Optimistic Create task implementation

In this case we don't know what ID will be generated on BE, so we need to generate our own on client and replace it in Success Action

import { optimisticUpdate } from '@nrwl/angular';

import * as TasksActions from './tasks.actions';

...
  createTaskOptimistic$ = createEffect(() =>
    this.actions$.pipe(
      ofType(TasksActions.createTaskOptimistic),
      optimisticUpdate({
        run: (action) =>
          this.api.createTask(action.task.name, action.task.status).pipe(
            tap(() => this.message.success('Task created')),
            // needs another action for replacing Optimistic ID (oid)
            map((task) =>
              TasksActions.createTaskOptimisticSuccess({
                oid: action.task.id,
                task,
              })
            )
          ),
        undoAction: (action, error) => {
          // add error handling here
          console.error('Error createTask$');
          // call Undo Action
          return TasksActions.undoCreateTask({
            error,
            /**
            * notice the the Undo Action's payload - in this case ID is enough, 
            * we're just removing it from the TasksEntities
            **/
            id: action.task.id, 
          });
        },
      })
    )
  );
Optimistic success Optimistic error
optmstc-scss optmstc-err