0
votes

How would you implement optimistic updates in RxJS? See this example:

const todoIdSource: Subject<string> = new Subject();
const $todo = todoIdSource.pipe(
    tap(id => {
        const optimisticTodo: any = {
            userId: 1,
            id: id, // Use some value from the source observable
            title: "delectus aut autem",
            completed: false
        };
        // TODO force emit optimisticTodo! How? :)
    }),
    concatMap((id) => ajax('https://jsonplaceholder.typicode.com/todos/' + id)),
    map(response => response.response)
);

$todo.subscribe(value => console.log('TODO', value));

todoIdSource.next('1');

$todo does not emit until the ajax call completed. But I want it to emit immediately the optimistic value as soon as todoIdSource emits. In the end $todo should have emitted 2 values:

  • the optimistic value
  • the value from ajax result

Note:

  • The optimistic value should not trigger the ajax call.
  • The optimistic value should be able to use data from the source observable todoIdSource.

Is there an elegant way to accomplish that in RxJS? I could imagine to introduce a second Observable optimisticTodo$ and then merge it with $todo. But that does not feel elegant :)

2

2 Answers

3
votes

either

concatMap(id =>
    from(ajax("https://jsonplaceholder.typicode.com/todos/" + id)).pipe(
      map(response => response.response),
      startWith({...})
    )
  )

Keep in mind, startWith has to be the last operator ( or at least after map ). This makes sure that the value you pass there does not get mapped, and in this case it should not since you use the map to extract the response.

or

  concatMap(id =>
    merge(
      of({...}),
      from(ajax("https://jsonplaceholder.typicode.com/todos/" + id)).pipe(
        map(response => response.response)
      )
    )
  )

Both achieve the same thing :

Whenever todoIdSource emits, emit immediately the optimistic todo, and emit the actual todo when the ajax call ends.

I'd prefer the startWith approach.

stackblitz

Little addendum: I'm asssuming the subscriber is actually only interested in the current state at the moment.

You could add another

  scan((acc,todo) =>({...acc,...{[todo.id]:todo}}), {}),
  map(v => Object.keys(v).map(k => v[k])),

To achieve that, see addendum stackblitz

1
votes

You can achieve that behavior with the startWith operator.