0

I have several LWC components that share data with each other. I am sending messages across just fine between the sibling components and sharing data based on events and user actions.

However, I have not found a way to have send a message and wait for a response like we can for instance with Apex calls. Is there any way to implement a request/response or handshake messaging model with LWC messaging (versus pub/sub) such that I can send a message and block execution till the response comes, similar to async/await?

One technique I've been think of is to wrapping a method in a setTimeout such that the request goes out and I loop till the message handler on the subscription has updated a state variable saying the message was received but wanted to see if anyone has come up with a better option.

1
  • I'm mobile, but I'm totally going to answer this when I get home.
    – sfdcfox
    Oct 19, 2022 at 0:03

1 Answer 1

1

Pubsub is traditionally a "fire-and-forget" event mechanism. That means that you don't ordinarily know if there are any receivers, nor how many, and the "forget" part means that you don't generally remember what you sent anyways.

The obvious way to deal with this in the pubsub model is to have component A send an event, and when component B wants to respond, it sends another event. This can be tricky in the face of complicated component hierarchies where you have no way to know if A or B will be subscribed in time to get each other's messages.

The general workaround for that problem is to have A send messages and store them until B either asks for messages or confirms those messages by an appropriate response, and B will send a notification when it becomes available, allowing A to send any queued messages.

But, if you wanted to make pubsub able to return asynchronous responses with await/async, what would that look like?

You might start by looking at MessageChannel. This is supported in Locker Service and Lightning Web Security and is a standard mechanism. It allows you to communicate between any two HTML entities, so it's perfect for this kind of 1:1 communication. The worst part of this system is trying to come up with a way to assign both ports to the relevant components; they'd both have to call a common method that would create the MessageChannel and assign each MessagePort to the other. The best part: no need for async/await at all on each end.

However, let's say you really wanted to use promises. You really could do that, and it'd be as simple as writing a few methods:

// Stores mapping of callbacks.
const events = {};
export function subscribe(event, callback) {
  const eventSubscribers = events[event] || (events[event] = []);
  eventSubscribers.push(new WeakRef(callback));
}
// Special feature: we never need to unsubscribe.

// Now, let's post messages!
export function publish(event, message) {
  const eventSubscribers = events[event];
  if (eventSubscribers) {
    // Remove dead subscribers
    events[event] = eventSubscribers.filter((subscriber) => subscriber.deref());
    // Nothing left to store
    if (eventSubscribers.length === 0) {
      delete events[event];
    } else {
      return Promise.allSettled(
        eventSubscribers.map((callback) => callback.deref()(message))
      );
    }
  }
  return Promise.resolve();
}

This probably isn't foolproof, and I haven't tested it, but it should provide a rather clean interface for returning all the values from any number of subscribers to a channel. You still have to deal with race conditions (you don't know how many subscribers are ready, if any), but this should serve as a starting point.

Other systems are possible, too. Keep in mind that static storage (the area outside of the export default class X) is shared with all instances of a component. You can purpose this into an area to share data between components, or even build methods that do exactly what you propose: wait until some data is available before calling a resolve method.

I definitely don't recommend using a polling loop. They are expensive and tend to lag the system. Just go with Promises, they tend to work a lot better in most situations.

3
  • Awesome! Thanks! So the static storage can be shared between different component types as long as all are loaded on the same page? For instance, if they all share a common library component. I'll try that. I changed my question, was so fried last night. Yes I'm looking to do request/response not pub/sub. Thanks again for this, I'll report on my results.
    – Shanerk
    Oct 19, 2022 at 13:10
  • @ShaneKenyon In a project I have now, I used some static storage to pass around objects in a pubsub model, it works out well. If you do decide to share objects this way, remember to import them first in every component that uses them to make sure they're initialized in time.
    – sfdcfox
    Oct 19, 2022 at 13:20
  • can you share an example? I have a shared component lib with a static object defined const timesheets = new Map(); I put that object in my export and in the consuming components I am importing it, but it seems each component has its own instance.
    – Shanerk
    Oct 19, 2022 at 13:36

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .