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)
Great article! I wanted to check if the trailing comma in the destructuring assignment is necessary in rxjs?
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...