Search code examples
angularrxjsreactivex

Some sort of to-do list with RxJS


I'm currently using RxJS in an angular project. In this project, I have a list that should act as a to-do list :

  1. In the beginning, the list is filled with 5 items.
  2. The user can remove an element.
  3. When an element is removed, a new element is fetched.
  4. When the list is empty, new elements are fetched.

I managed to do so using 3 observables and 1 state:

// api.fetchElement -> ({ count: number, exclude: string[] }) => Observable<Element[]>
// removedElements$ -> Subject<string>

let elements = []

const initialElements$ = api.fetchElement({ count: 5 })

// removedElementId$ is a subject of string
// in which element ids are produced.
const reloadedElements$ = removedElementId$.pipe(
    // My API is eventualy consistent, so I need to exclude the removed items
    // for a short period.
    mergeMap(id => api.fetchElement({ count: 5, exclude: [ id ] })
)

const reloadedElementsWhenEmptyList$ = interval(3000).pipe(
    filter(() => elements.length < 5),
    mergeMap(() => api.fetchElement({ count: 5 - elements.length, exclude: elements.map(e => e.id) })
)

initialElements$.subscribe({
    next: newElements => elements = newElements
})

reloadedElements$.subscribe({
    next: newElements => elements = newElements
})

reloadedElementsWhenEmptyList$.subscribe({
    next: newElements => elements = newElements
})

The current implementation works, but I'm wondering if there is a way to do the same behaviour but without having to use the final state elements in the observable operators?

I've searched for operators in the RxJS docs, but I failed to find any operators or combinations of operators that satisfy my needs.


Solution

  • Your settup is a bit bizarre and I can't help but wonder if there's a better way to structure this. Sadly, there's just not enough here to know.

    Either way, you can rid yourself of the global state elements by creating an observable that emits the most recent elements array when subscribed. In this example implementation I've called it elements$

    const initialElements$ = api.fetchElement({ count: 5 });
    
    // removedElementId$ is a subject of string
    // in which element ids are produced.
    const reloadedElements$ = removedElementId$.pipe(
        // My API is eventualy consistent, so I need to exclude the removed items
        // for a short period.
        mergeMap(id => api.fetchElement({ 
          count: 5, 
          exclude: [ id ] 
        }))
    );
    
    const reloadedElementsWhenEmptyList$ = interval(3000).pipe(
        switchMap(_ => elements$),
        filter(elements => elements.length < 5),
        mergeMap(elements => api.fetchElement({ 
          count: 5 - elements.length, 
          exclude: elements.map(e => e.id) 
        }))
    );
    
    const elements$ = merge(
      initialElements$,
      reloadedElements$,
      reloadedElementsWhenEmptyList$
    ).pipe(
      startWith([]),
      shareReplay(1)
    );
    

    You'll notice that when I needed access to your old elements array, I just subscribed to elements$.

    If you're using TypeScript, it might take some extra work to make sure your types align here. Again, there's not enough info in your example to do this as writ, but I'll leave that exercise to you.