Is there an RxJS trick to combine observables and manage state?

Currently encountering a common issue involving asynchronous execution, state management, and indentation complexities.

Imagine a scenario where a REST call is made to add user information (user_info) to a user, notify their contacts of the change, and return the updated user information in the response. However, the user object only contains the IDs of the user_info objects through a 1 to n relation.

Sequence of Calls:

request -> saveUserInfo -> updateUser -> notifyContacts -> response

Functions saveToDb, updateUser, and notifyContacts are asynchronous and cannot be easily composed due to different inputs, outputs, and execution order dependencies.

Below are simplified function headers:

function saveUserInfo(payload): Promise<UserInfo[]> { /*...*/ }
function updateUser(user_id, user_infos): Promise<User> { /*...*/ }
function sendNotification(user, user_infos): Promise<void> { /*...*/ }

While working on the request handler, I relied heavily on mergeMap to subscribe to inner observables for asynchronous actions. Here's an example:

function handleRequest(payload, user_id) {
  return saveUserInfo(payload).pipe(
    mergeMap(user_infos =>
      from(updateUser(user_id, user_infos)).pipe(
        mergeMap(user =>
          from(notifyContacts(user, user_infos)).pipe(map(() => user_infos))
        )
      )
    )
  )
}

Not entirely satisfied with this approach, as it may become complex with additional logic in the future. Explored RxJS documentation but could not find a more elegant solution to chain these asynchronous calls. The challenge lies in articulating the problem concisely, prompting this inquiry.

Any suggestions for a better solution? Seeking a pure RxJS approach, preferably without helper functions.

Answer №1

When utilizing a high-order mapping operator (for example mergeMap and its counterparts) and if the provided callback returns a promise, there is no need to manually do () => from(promise). mergeMap handles this process automatically.

Keeping this in mind, I would modify the code below:

function handleRequest(payload, user_id) {
  return saveUserInfo(payload).pipe(
    mergeMap(user_infos =>
      from(updateUser(user_id, user_infos)).pipe(
        mergeMap(user =>
          from(notifyContacts(user, user_infos)).pipe(map(() => user_infos))
        )
      )
    )
  )
}

to this:

function handleRequest(payload, user_id) {
  return from(saveUserInfo(payload)) // `saveUserInfo(payload)` returns a promise
    .pipe(
      mergeMap(user_infos => forkJoin(of({ user_infos }), updateUser(user_id, user_infos)),
      mergeMap(([acc, user]) => forkJoin(of({ ...acc, otherPropsHere: true }), notifyContacts(user, user_infos))),
      map(([acc]) => acc['user_infos'])
    ),
  )
}

forkJoin will output an array after all its provided ObservableInputs have emitted at least one value and completed.

As illustrated, the first element of the returned array serves as the accumulator.

Answer №2

After exhausting my search for a solution online, I decided to create a more advanced function that could act as a pipeable operator to potentially resolve my issue. I named this function sourceMap.

The goal of sourceMap is to subscribe to an inner observable using mergeMap and then merge the result into a state object with key-value pairs.

const sourceMap = <S, T extends Record<string, any>>(
  predicate: (acc: T) => Promise<S> | Observable<S>,
  key?: keyof T
) => {
  return mergeMap((acc: T) =>
    from(predicate(acc)).pipe(map(res => (key ? { ...acc, [key]: res } : acc)))
  )
}

The implementation of this function is demonstrated below:

function handleRequest(payload, user_id): Observable<UserInfo[]> {
  const state$ = of({ user_infos: [] as UserInfo[], user: {} as User })

 return state$.pipe(
    sourceMap(() => saveUserInfo(user_id, payload), 'user_info'),
    sourceMap(({ user_info }) => updateUser(user_id, user_info), 'user'),
    sourceMap(({ user_info, user }) => notifyContacts(user, user_info)),
    pluck('user_info')
  )
}

I am still on the lookout for a more optimal solution!

Answer №3

Here is an alternative approach:

function storeUserData(data: Data): Promise<UserData[]>;
function modifyUser(user_id: string, user_data: UserData[]): Promise<User>;
function notifyUser(user: User, user_data: UserData[]): Promise<void>;

function processRequest(data: Data, user_id: string): Observable<UserData[]> {
  return from(storeUserData(data)).pipe(
    switchMap(user_data => modifyUser(user_id, user_data).then(user => ({ user, user_data }))),
    switchMap(({ user, user_data }) => notifyUser(user, user_data).then(() => user_data))
  );
}

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

Invoking a Typescript function from the Highcharts load event

Struggling to call the TypeScript function openDialog() from the events.load of Highcharts? Despite using arrow functions, you are running into issues. Take a look at the code snippet below: events: { load: () => { var chart : any = this; ...

Maintaining the dropdown in the open position after choosing a dropdown item

The dropdown menu in use is from a bootstrap framework. See the code snippet below: <li id="changethis" class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown>LINK</a> <ul class="dropdown-menu"> <li id ...

Angucomplete Alternative solves the challenge of accessing remote URLs

I have been using the Angucomplete Alt directive for creating an autocomplete feature. It has been working well so far, but now I want to customize a specific request to be sent to my server. <div angucomplete-alt id="input-name" ...

When utilizing an 'imported' asynchronous function, make sure to clean up the useEffect

Please do not mistake this for a duplicate. Despite my thorough search on various blogs and sources, I have yet to find a solution. My primary question is 'how can I address the error of react state change in unmounted component, and also cancel an a ...

Trigger an immediate Primefaces update using a JavaScript function

Having an issue where: upon page load, I attach a 'keypress' event listener to every input field on the page. The goal is to check for special characters in the input and remove them. Below is the function that executes on page load: function ...

An error has occurred: Noty (notification library) is not defined in this AngularJS Web Application

I am currently diving into the world of AngularJS and building a web application from scratch. As a newbie to AngularJS, I want to point out that I might be missing something crucial. An issue has arisen: After installing the Noty library (npm install no ...

My Vuex component is not updating when the state changes

My component is not reacting to Vuex store changes when I edit an existing element, even though it works fine when adding a new element. After hours of debugging and trying everything, I've realized that it might have something to do with deep watchin ...

Adding up the values of an array of objects by month using ReactJS

To start off, I'm using ChartJS and need to create an array of months. Here's how it looks: const [generatedMonths, setGeneratedMonths] = useState<string[]>([]) const [totalValues, setTotalValues] = useState<number[]>([]) const month ...

Sharing data between AngularJS and D3 with JSON - a guide

When working on my application controller, I typically send a request to my API. This is what it usually looks like: .state('state1', { url: '/datas/:id', templateUrl: 'myurl.com', title: 'title', ...

The functionality in the React Native code that uploads images to an S3 bucket is currently failing to initiate the network request

I have been using a redux-observable epic to upload images to my AWS S3 bucket through react-native-aws3. It has been functioning smoothly for quite some time. However, recently it seems to have stopped entering the .map and .catch blocks of code. I suspec ...

Value of the object is currently not defined

Having difficulty determining values in a manner that allows for later accessibility. When defining Search first, Search.commands[3] becomes undefined. On the other hand, defining commandList first results in commandList.commands[0] being undefined. Is t ...

The latest update of MS CRM 2013 now includes a version number for WebResources that are of script

I came across an unusual issue in MS CRM 2013 that seems to be intentional, and I need some assistance in finding a workaround for it. The problem is that calling the getScript jQuery method from a WebResource is not possible. In CRM, a version string is ...

The Updating Issue: Angular 2 Table Fails to Reflect Value Changes

I have initialized a table with user details using the ngOnInit() method. When I click on the "create user" button, it opens a form to add a new user to the database. However, the table does not update automatically with the new user's information. H ...

Can we verify if this API response is accurate?

I am currently delving into the world of API's and developing a basic response for users when they hit an endpoint on my express app. One question that has been lingering in my mind is what constitutes a proper API response – must it always be an o ...

Having trouble retrieving the ID of a button?

I'm attempting to retrieve the ID of a button, but I seem to be getting the ID of the surrounding div instead. This is not the desired outcome. Here's my current approach: HTML <div class="container container-about container-login"> ...

Incorporate an image into your webpage with the Fetch API by specifying the image link - JavaScript

I've been attempting to retrieve an image using the imageLink provided by the backend server. fetchImage(imageLink) { let result; const url = `https://company.com/internal/document/download?ID=${imageLink}`; const proxyurl = 'https:/ ...

Error: Webpack is unable to load PDF file: Module parsing unsuccessful. A suitable loader is required to manage this file format

I am relatively new to using webpack for my projects. Recently, I wanted to incorporate a feature that involved displaying PDFs. After some research, I came across the "react-pdf" library and decided to give it a try. While everything worked smoothly in a ...

The attribute 'listen' is not a valid property for the data type 'NavigateFunction'

Just diving into the world of Typescript and react, I recently made the switch from useHistory to useNavigate in react-router-dom v6. However, when using the navigate.listen(e) method inside the useEffect hook, I am encountering the error "Property ' ...

Struggling to comprehend the filtering arguments in VueJS?

While going through the VueJS Filter(orderby) API documentation, I came across some confusion regarding the arguments. Below is a sample for reference: Arguments: {String | Function} targetStringOrFunction "in" (optional delimiter) {String} [...s ...

What is the method to retrieve content from a different domain using .load()?

When I try to retrieve data from a different domain using .load() or other jQuery ajax functions, it doesn't seem to work. However, accessing URLs on my own domain works perfectly fine. I've heard about a workaround involving PHP and setting up ...