DEV Community

Cover image for Getting a recursive data structure asynchronously with RxJS
Ran Lottem
Ran Lottem

Posted on

Getting a recursive data structure asynchronously with RxJS

In your app, you might want to save a recursive data structure - such as a folder tree. These folders could be identified by an id attribute, and contain an array of their sub-folders:

export interface Folder {
  id: number;
  name: string;
  children: Folder[];
}

However, on the server the data for each folder might only contain the identifiers for the sub-folders:

export interface ServerData {
  id: number;
  name: string;
  children: number[];
}

This means that when we write a function to return an observable of a folder, given its id, we'll first need to also get all of its sub-folders from the server before we can return the complete object.

Let's look at some mock data, and a function that gets this mock data asynchronously:

export const results: ServerData[] = [
  { id: 0, name: "first", children: [1, 2, 3] },
  { id: 1, name: "second", children: [4] },
  { id: 2, name: "third", children: [] },
  { id: 3, name: "fourth", children: [] },
  { id: 4, name: "fifth", children: [] }
];

export function getFromServer(id: number): Observable<ServerData> {
  return of(results[id]);
}

Now, let's try to transform the ServerData we get into a Folder, including its children attribute:

export function getRecursive(id: number): Observable<Folder> {
  return getFromServer(id).pipe(
    map(data => ({
      id: data.id,
      name: data.name,
      // oops, wrong type!
      children: data.children.map(childId => getRecursive(childId))
    }))
  );
}

This implementation doesn't work, because the children attribute above is actually an Observable<Folder> array. We need to combine the result from getting the parent folder with all of the results of the recursive calls to the ids of the parent's sub-folders.

export function getRecursive(id: number): Observable<Folder> {
  return getFromServer(id).pipe(
    map(data => ({
      parent: { name: data.name, id: data.id, children: [] },
      childIds: data.children
    })),
    flatMap(parentWithChildIds => forkJoin([
      of(parentWithChildIds.parent),
      ...parentWithChildIds.childIds.map(childId => getRecursive(childId))
    ])),
    tap(([parent, ...children]) => parent.children = children),
    map(([parent,]) => parent)
  );
}

What we do here is create parent without any children, and move it and its child ids further along the pipe. Then, we create an array of observables, containing the parent we already have (that's the of part) and all of the getRecursive calls for each of the sub-folder ids. Once we have the return values from each of those recursive calls, we set parent.children = children, and use array destructuring to return just the parent folder, which now has its children attribute set correctly.

A short test to show this function in action:

describe('test recursive observables', () => {
  test('test', () => {
    getRecursive(0).subscribe(data => {
      console.log(data);
      console.log(data.children.find(f => f.id === 1).children);
    });
  })
})

And the output:

    parent folder: { name: 'first',
      id: 0,
      children:
       [ { name: 'second', id: 1, children: [Array] },
         { name: 'third', id: 2, children: [] },
         { name: 'fourth', id: 3, children: [] } ] }

    children of child with id 1: [ { name: 'fifth', id: 4, children: [] } ]

Top comments (2)

Collapse
 
trentmilton profile image
Trent Milton

Great article! I wanted to check if the trailing comma in the destructuring assignment is necessary in rxjs?

Collapse
 
krumpet profile image
Ran Lottem

I don't know if it has anything to do with rxjs specifically, but I just checked and it indeed works without the trailing comma - I thought it would be required to specify only naming the first item in the array, but that is not so. It can be used to specify other positions, see here:

basarat.gitbooks.io/typescript/doc...