Share edited collection with Observer

The challenge

Imagine creating an Angular service that needs to expose an Observable<number[]> to consumers:

numbers: Observable<number[]>;

Our requirements are:

  1. Receive the latest value upon subscription
  2. Receive the entire array every time it is modified and published

To achieve #1, internally our Observable<number[]> should be at least a BehaviorSubject<number[]>.

But how do we address requirement #2? Let's say we need to implement a method publishNumbersChange() which gets called whenever there's a change to the array:

private publishNumbersChange() {
    // Get current numbers array
    ...        

    changeArray();

    // Publish the updated numbers array
    ...
}

The question at hand

What is the preferred pattern in RxJS 5 for publishing modified arrays based on their previous state?

Since this query stems from my work with Angular, let's delve further:
How does Angular (and similar frameworks using RxJS) handle updating arrays within Observables?
Do they maintain a separate copy of the current array?

Considerations

One approach could involve storing a separate reference to the array to ensure constant access. However, this might not align with the principles of RxJS (as it introduces external state).

Alternatively, we could try the following strategy:

private publishNumbersChange() {
    // Subscribe to get the latest array value
    const subscription: Subscription = this.numbers.subscribe((numbers: number[]) => {
        // Modify the array
        changeArray();

        // Update the stream with the new array
        this.numbers.next(numbers);
    });

    // Unsubscribe to prevent memory leaks
    subscription.unsubscribe();
}

One issue with this approach (aside from its complexity and reusability) is the potential "race condition" between executing the subscriber callback and unsubscribing. This uncertainty makes it less than ideal.

Answer â„–1

If you're in need of an operator, scan might be the one you're searching for.

let subjectArray = new BehaviorSubject([]);
let array$ = subjectArray.scan((fullArr, newVal) => fullArr.concat([newVal]), [])

Scan function accumulates values over time within an observable stream, where each item in the stream receives the previously emitted value and the current value as inputs. It then applies a function to them and emits the result. For instance, the above example takes a new value and adds it to your full array, with the second parameter initializing the array as empty.

However, if you find this limitation restricting, you can get creative:

let subjectArray = new BehaviorSubject([]);
let array$ = subjectArray.scan((fullArr, {changeFunc, input}) => changeFunc(fullArr, input), []);

Now, by passing an "action" that contains a modifier function specifying how you want to alter the full array, along with any additional data required by the modifier function.

For example, you could do the following:

let changeFunc = (full, item) => full.splice(full.indexOf(item), 1);
subjectArray.next({changeFunc, input: itemToRemove});

This action removes the specified item. You can extend this approach to accommodate any array modifications.

One thing to note about scan is that subscribers only receive the accumulated value from the point they subscribed onwards. This results in:

let subjectArray = new BehaviorSubject([]);
let array$ = subjectArray.scan((fullArr, {changeFunc, input}) => changeFunc(fullArr, input), []);
let subscriber1 = array$.subscribe();
//subscriber1 gets []
let changeFunc = (full, val) => full.concat([val]);
subjectArray.next({changeFunc, input: 1});
//subscriber1 gets [1]
subjectArray.next({changeFunc, input: 2});
//subscriber1 gets [1,2]
let subscriber2 = array$.subscribe();
//subscriber2 gets [2]
subjectArray.next({changeFunc, input: 3});
//subscriber1 gets [1,2,3]
//subscriber2 gets [2,3]

In this scenario, the BehaviorSubject only stores the second event, not the entire array, while scan retains the complete array. As a result, the second subscriber only receives the second action because it wasn't subscribed during the first action. To address this issue, consider implementing a persistent subscriber pattern:

let subjectArray = BehaviorSubject([]);
let modifySubject = new Subject();
modifySubject.scan((fullArr, {changeFunc, input}) => changeFunc(fullArr, input), []).subscribe(subjectArray);

To make modifications, use next on modifySubject:

let changeFunc = (full, val) => full.concat([val]);
modifySubject.next({changeFunc, input: 1});

Your subscribers will obtain the array from the array source:

subscriber1 = subjectArray.subscribe();

In this setup, all array changes flow through the modifySubject, which broadcasts them to the BehaviorSubject for storage and distribution to subscribers. The BehaviorSubject remains persistently subscribed to the modifySubject and serves as the sole subscriber to ensure the entire history of actions is preserved.

Below are a few sample usages with the mentioned setup:

// insert 1 at the end
let changeFunc = (full, value) => full.concat([value]);
modifySubject.next({changeFunc, input: 1});

// insert 1 at the start
let changeFunc = (full, value) => [value].concat(full);
modifySubject.next({changeFunc, input: 1});

// remove 1
let changeFunc = (full, value) => full.splice(full.indexOf(value), 1);
modifySubject.next({changeFunc, input: 1});

// change all instances of 1 to 2
let changeFunc = (full, value) => full.map(v => (v === value.target) ? value.newValue : v);
modifySubject.next({changeFunc, input: {target: 1, newValue: 2}});

You can encapsulate these functions in a "publishNumbersChange" method. The implementation varies based on your requirements—options include creating functions like:

insertNumber(numberToInsert:number) => {
   let changeFunc = (full, val) => full.concat([val]);
   publishNumbersChange(changeFunc, numberToInsert);
}

publishNumbersChange(changeFunc, input) => {
   modifySubject.next({changeFunc, input});
}

Alternatively, you can define an interface, create classes, and leverage those:

publishNumbersChange({changeFunc, input}) => {
   modifySubject.next({changeFunc, input});
}

interface NumberArrayModifier {
    changeFunc: (full: number[], payload:any) => number[];
    input: any;
}

class InsertNumber implements NumberArrayModifier {
    changeFunc = (full: number[], payload: number): number[] => full.concat([payload]);
    input: number;
    constructor(numberToInsert:number) {
        this.input = numberToInsert;
    }
}

publishNumbersChange(new InsertNumber(1));

This approach can be extended to any array modification. A final tip: lodash proves invaluable when defining modifiers in such systems.

Considering an Angular service context, the following represents a simple yet somewhat non-reusable implementation:

const INIT_STATE = [];
@Injectable()
export class NumberArrayService {
    private numberArraySource = new BehaviorSubject(INIT_STATE);
    private numberArrayModifierSource = new Subject();
    numberArray$ = this.numberArraySource.asObservable();

    constructor() {
        this.numberArrayModifierSource.scan((fullArray, {changeFunc, input?}) => changeFunc(fullArray, input), INIT_STATE).subscribe(this.numberArraySource);
    }

    private publishNumberChange(changeFunc, input?) {
        this.numberArrayModifierSource.next({changeFunc, input});
    }

    insertNumber(numberToInsert) {
        let changeFunc = (full, val) => full.concat([val]);
        this.publishNumberChange(changeFunc, numberToInsert);
    }

    removeNumber(numberToRemove) {
        let changeFunc = (full, val) => full.splice(full.indexOf(val),1);
        this.publishNumberChange(changeFunc, numberToRemove);
    }

    sort() {
        let changeFunc = (full, val) => full.sort();
        this.publishNumberChange(changeFunc);
    }

    reset() {
        let changeFunc = (full, val) => INIT_STATE;
        this.publishNumberChange(changeFunc);
    }
}

Usage involves subscribing to numberArray$ and modifying the array by invoking functions. This straightforward pattern allows for easy extension to tailor functionality according to requirements. It safeguards access to your number array by ensuring modifications align with the API, state, and maintaining consistency between subjects.

How can this be made more generic/reusable?

export interface Modifier<T> {
    changeFunc: (state: T, payload:any) => T;
    payload?: any;
}

export class StoreSubject<T> {
    private storeSource: BehaviorSubject<T>;
    private modifierSource: Subject<Modifier<T>>;
    store$: Observable<T>;

    publish(modifier: Modifier<T>): void {
        this.modifierSource.next(modifier);
    }

    constructor(init_state:T) {
        this.storeSource = new BehaviorSubject<T>(init_state);
        this.modifierSource = new Subject<Modifier<T>>();
        this.modifierSource.scan((acc:T, modifier:Modifier<T>) => modifier.changeFunc(acc, modifier.payload), init_state).subscribe(this.storeSource);
        this.store$ = this.storeSource.asObservable();
    }
}

The modified service would appear as follows:

const INIT_STATE = [];
@Injectable()
export class NumberArrayService {
    private numberArraySource = new StoreSubject<number[]>(INIT_STATE);
    numberArray$ = this.numberArraySource.store$;

    constructor() {
    }

    insertNumber(numberToInsert: number) {
        let changeFunc = (full, val) => full.concat([val]);
        this.numberArraySource.publish({changeFunc, payload: numberToInsert});
    }

    removeNumber(numberToRemove: number) {
        let changeFunc = (full, val) => full.splice(full.indexOf(val),1);
        this.numberArraySource.publish({changeFunc, payload: numberToRemove});
    }

    sort() {
        let changeFunc = (full, val) => full.sort();
        this.numberArraySource.publish({changeFunc});
    }

    reset() {
        let changeFunc = (full, val) => INIT_STATE;
        this.numberArraySource.publish({changeFunc});
    }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The response of the Typescript Subscription function

I'm struggling with retrieving the subscribe array in NG2. Being new to typescript, I find it difficult to understand how to pass variables between functions and constructors. This is what my code currently looks like: export class RosterPage exten ...

What is the process for implementing a unique Angular theme across all components?

I created a unique Angular theme called 'my-theme.scss' and added it to the angular.json file. While it works with most Angular Elements, I am facing issues getting it to apply to certain HTML Elements inside Angular Components. For example: < ...

Adjust the size of an image post-render with the help of Angular or jQuery

Below is my current HTML code: <img width="150" height="150" src="https://d.pcd/image/578434201?hei=75&amp;wid=75&amp;qlt=50,0&amp;op_sharpen=1&amp;op_usm=0.9,0.5,0,0" ng-src="https://d.pcd/image/578434201?hei=75&amp;wid=75&amp; ...

Is there a way to show a string value stored in Java onto an Angular 8 display?

Hey there! I am just starting out with angular 8 and have a java project where I'm using JDBC to connect to my beloved MySQL database. I have some valuable data stored in a MySQL table that I access in java and store in strings (or even a list). Now, ...

Angular error message: Trying to access the property 'name' of an undefined object leads to a TypeError

I'm having trouble phrasing this question differently, but I am seeking assistance in comprehending how to address this issue. The error message I am encountering is as follows: TypeError: _co.create is not a function TypeError: Cannot read property ...

Seamless integration of Applitools with WebdriverIO

Seeking assistance to integrate Applitools with my WebdriverIO project. Find the specifications below: Node Version: v12.18.2 NPM Version: 6.14.5 WebdriverIO Version: 6.3.6 The service in my wdio file is configured as follows: services: ['selenium-s ...

Issue with Ng-style not functioning properly when setting background color

I am struggling to set the background of an element using a configuration object with ng-style. For some unknown reason, I can't seem to make it work and I'm finding it really perplexing. The element I'm attempting to configure: <div id ...

Replacing a component dynamically within another component in Angular 2

I am currently working on integrating a component within another component that has its own view. The component being loaded will be determined dynamically based on specific conditions. Essentially, I am looking to replace one component with another compo ...

What is the recommended return type in Typescript for a component that returns a Material-UI TableContainer?

My component is generating a Material-UI Table wrapped inside a TableContainer const DataReleaseChart = (): React.FC<?> => { return ( <TableContainer sx={{ display: 'grid', rowGap: 7, }} > ...

CORS has restricted access to the XMLHttpRequest, despite the backend being configured correctly

I have a Flask backend integrated with React frontend. I encountered an issue while attempting to make a POST request to my backend. The error message states: Access to XMLHttpRequest at 'http://127.0.0.1:5000/predict' from origin 'http://lo ...

The configuration of the property has not been declared (Error: <spyOnProperty>)

Imagine having a MenuComponent @Component({ selector: 'cg-menu', templateUrl: './menu.component.html', styleUrls: [ './menu.component.scss' ] }) export class MenuComponent implements OnInit { menu: MenuItem[]; isLog ...

What is the best way to expand the subcategories within a list?

I have successfully implemented a script to open my hamburger menu. However, I am facing an issue with subitems as some list items contain them. How can I modify the script to ensure that the subitems expand when clicked instead of just hovering? functi ...

Using JavaScript to load the contents of a JSON file

I attempted to display data from a JSON file on an HTML page using jQuery / Javascript. However, each time I load the page, it remains blank. Below is the code snippet: index.html <!DOCTYPE html> <html> <head> <meta conten ...

Can you please provide the Typescript type of a route map object in hookrouter?

Is there a way to replace the 'any' type in hookrouter? type RouteMap = Record<string, (props?: any) => JSX.Element>; Full Code import { useRoutes, usePath, } from 'hookrouter' //// HOW DO I REPLACE any??? type RouteMap = ...

Angular's ngbdatepicker encountering RangeError: Call stack size has surpassed maximum limit

Below is the code snippet used in my angular component template. <input type="text" (click)="dp.toggle()" ngbDatepicker #dp="ngbDatepicker" id="myDatePicker" autocomplete="off" placeholder= ...

Showcase JSON data within a designated div

As someone new to HTML, JavaScript and Vue, I'm unsure if my issue is specific to Vue or can be resolved with some JavaScript magic. I have a Node.js based service with a UI built in Vue.js. The page content is generated from a markdown editor which ...

Personalize the Facebook like button to suit your preferences

I have posted my code on jsfiddle. You can find the link here: http://jsfiddle.net/QWAYE/1/. I have also included another sample code in this link: http://jsfiddle.net/MCb5K/. The first link is functioning correctly for me, but I want to achieve the same a ...

Dividing a string yields varying outcomes when stored in a variable compared to when it is displayed using console.log()

When the `$location` changes, a simple function is executed as shown below. The issue arises when the assignment of `$location.path().split("/")` returns `["browser"]` for `$location.path() == "/browser"`, but when run directly inside the `console.log`, ...

The video continues playing even after closing the modal box

I am facing an issue with my code where a video continues to play in the background even after I close the modal. Here is the code snippet: <div class="modal fade" id="videoModal" tabindex="-1" role="dialog" aria- ...

What sets returning a promise from async or a regular function apart?

I have been pondering whether the async keyword is redundant when simply returning a promise for some time now. Let's take a look at this example: async function thePromise() { const v = await Inner(); return v+1; } async function wrapper() ...