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 |
---|---|
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 |
---|---|
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 |
---|---|
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 |
---|---|
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 |
---|---|
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 |
---|---|
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 |
---|---|