Blog

Asynchronous JavaScript execution in Vaadin Flow

By  
Matti Tahvonen
Matti Tahvonen
·
On Feb 15, 2024 5:47:43 PM
·

Web applications built entirely in Java. That promise holds true when core Vaadin Flow components and browser APIs are enough. However, if you’re creating custom components or using modern asynchronous browser APIs, you’re likely to encounter a very different kind of “Promise.”

JavaScript's asynchronous nature is evident not just through the use of Promise and the related async and await keywords, but also in how legacy JavaScript frequently employs callbacks (and/or setTimeout) to delay the resolution of the requested return value until a future time. In the single-threaded world of JavaScript, asynchronous APIs with callbacks (and Promises) are the only way to achieve concurrency (virtual parallelism), which is often needed for a decent user experience. 

Let’s check out five methods to return values from the browser side asynchronously. The first two can be done using core APIs. However, the last method, though not least in importance, requires an add-on or a bit of manual copy-paste dependency management.

All the examples below use the fetch API in the browser to make an HTTP request for a JSON object. The JSON object is then sent “back” to the server and then mapped to a Java DTO there. This is an artificial yet simple and easy-to-understand example of how modern asynchronous JS APIs work. Typically, Vaadin developers might face Promise-based APIs in non-trivial JS component APIs or when accessing newer browser APIs like WebAuthn or Web Bluetooth.

Method 1: Publish a callback method using @ClientCallable

The @ClientCallable annotation exposes your component API to the special $server property of the underlying elements. When you receive the result from your asynchronous callback, you can use this property to send your values back to the server:

private void returnValueFromClientCallable() {
   getElement().executeJs("""
           const el = this; // closure to element
           return fetch("/personjson").then(response => {
                   return response.json();
               }).then(json => {
                   const jsonStringToServer = JSON.stringify(json);
                   // call the proxy to exposed Java method
                   el.$server.clientCallable(jsonStringToServer);
               });
           """);
}

@ClientCallable
private void clientCallable(String msg) {
   try {
       showDtoInUI(new ObjectMapper().readValue(msg, PersonDto.class));
   } catch (JsonProcessingException e) {
       throw new RuntimeException(e);
   }
}

This looks a bit ugly, but it is simple, and it works. A tiny downside is that JS tinkerers can easily discover the callback using a browser inspector and abuse it. Also, depending on your use case, you might need to create and pass along some identifiers and maintain mapping on the server side to connect the data with the correct callback there.

Similar to the return values from executeJs calls, which are synchronous, methods annotated with @ClientCallable can only receive basic data types and an untyped Vaadin-specific JsonValue object. Thus, we need to serialize the data into a string for transport and then deserialize it on the server side using Jackson’s ObjectMapper.

Method 2: Listen to custom JS events

The Element API provides powerful mechanisms for listening to DOM events that occur on the browser side. Events can also carry your data, and you can trigger custom events from virtually anywhere in your JS code. You just need to keep a reference to the element that you are listening to on the server side.

private void returnValueUsingEvent() {
   getElement().addEventListener("my-custom-event", e -> {
       String json = e.getEventData().getString("event.data");
       try {
           showDtoInUI(new ObjectMapper()
                   .readValue(json, PersonDto.class));
       } catch (JsonProcessingException ex) {
           throw new RuntimeException(ex);
       }
   }).addEventData("event.data");
   getElement().executeJs("""
       const el = this; // closure to element
       return fetch("/personjson").then(response => {
               return response.json();
           }).then(json => {
               const event = new Event("my-custom-event");
               const jsonStringToServer = JSON.stringify(json);
               event.data = jsonStringToServer;
               el.dispatchEvent(event);
           });
       """);
}

Events are probably the most powerful method in Vaadin for moving data from the browser to the server, especially when working with custom components. For example, there are APIs for debouncing, and events are automatically paused for disabled components to enhance security. Fetching a single value from the client, this approach is usually overkill. However, if you’re subscribing to a stream of values, it might be the way to go.

Method 3: Return a Promise in the executeJs script

Vaadin’s Element.executeJs API contains a hidden gem. It’s so well-hidden that I hadn’t discovered it until recently,  prompting me to create a pull request to make it more visible. Within the JS snippet provided, you can return a JS Promise. If a Promise is returned, the handlers configured to the PendingJavaScriptResult – the return value of the executeJs method – are notified only after the value is resolved on the client side.

private void returnValueUsingAPromise() {
   getElement().executeJs("""
           return fetch("/personjson").then(response => {
                   return response.json(); // this is also a Promise
               }).then(json => {
                   const jsonStringToServer = JSON.stringify(json);
                   return jsonStringToServer;
               });
           """).then(String.class, json -> {
               try {
                   showDtoInUI(new ObjectMapper()
                           .readValue(json, PersonDto.class));
               } catch (JsonProcessingException ex) {
                   throw new RuntimeException(ex);
               }
           });
}

In this trivial case, the difference from previous solutions might not seem significant.  However, in real-life scenarios, you often have parameters for the JS call and some Java code you want to execute based on this exact execution. Now, you can directly chain your Java API (for example, CompletableFuture or custom callbacks) without needing to manage extra ID mappings in your code.

If your JS API doesn’t use Promises but relies on custom callbacks instead, you’ll need to do a bit more JS tinkering. In such cases, you can instantiate a new Promise and resolve it in the callback. A trivial example using setTimeout that resolves and logs a String would be:

getElement().executeJs("""
       return new Promise((resolve, reject) => {
           setTimeout(() => resolve("Your dear friend Jorma"), 1000);
       });
       """).then(String.class, System.out::println);

Method 4: Call the anonymous async method to utilize await

With the above, you can already do pretty much anything, but with long chained promises the JS code can end up pretty complex and hard to maintain. Many JavaScript developers and docs promote the usage of the await keyword in front of API calls returning a Promise. It is a language construct built around Promise objects that can significantly improve the readability of your asynchronous call chains.

The nasty thing is that the await keyword can only be used within a method that is decorated with the async keyword, and for the Element.executeJs(String) method, we are passing in only the method bodies. But there is a tiny hack we can do. By introducing an anonymous async method and applying that to the current context, we can make await work in our snippets and still refer to the element as this and possible parameter values.

private void returnValueUsingUsinganAnonymousAsyncFunction() {
     getElement().executeJs("""
             return (async () => {
                 // Now we can use await keyword to make asynchronous
                 // API look almost like a synchronous one
                 const response = await fetch("/personjson");
                 const jsObject = await response.json();
                 const jsonToServer = JSON.stringify(jsObject);;
                 return jsonToServer;
             }).apply(this, arguments); // anonymous async func -> Promise
             """).toCompletableFuture(String.class).thenAccept(jso-> {
                 try {
                     showDtoInUI(new ObjectMapper()
                             .readValue(jso, PersonDto.class));
                 } catch (JsonProcessingException ex) {
                     throw new RuntimeException(ex);
                 }
             });
}

The example above showcases another method for hooking logic to responses from JS executions. By calling the toCompletableFuture method, the result transforms to a more commonly used Java interface and might be directly more compatible with other Java APIs. 

Method 5: JsPromise helper from the Viritin add-on

While fiddling with the above possibilities when putting together one of my upcoming Vaadin + modern web API examples, I started to think our Element API might need a refresh. I took in the Viritin add-on (kind of a commons-* library for Vaadin, providing shortcuts and helpers for the core API) and prototyped with a more “Promise optimized” JS API. 

private void returnWithJsPromiseCompute() {
   JsPromise.compute("""
           const response = await fetch("/personjson");
           const jsObject = await response.json();
           return jsObject;
           """, PersonDto.class)
           .thenAccept(this::showDtoInUI);
}

The helper method shown above abstracts the hack from method four into a cleaner API (the JS parameter is interpolated into the method body of the anonymous async function) and adds seamless JSON handling for both ends. On the server side, the API returns the standard CompletableFuture to which you attach your handler logic and possible error handling. If you’re working with simple data types that don’t need to be serialized to JSON for transport, you can use better-typed variants like computeString, computeInteger, computeDouble, or computeBoolean instead.

If you think this kind of API enhancement would be helpful in Vaadin Flow, please feel free to give a thumbs up to my newly created enhancement issue. Update: no votes needed, Flow team was super agile and fix is already in. Instead, test await keyword in your executeJS calls with the next 24.4 alpha release! Better typing & seamless JSON mapping are still the TODO list thouw!

Wrapping up 

There are many methods to return values asynchronously from the browser side. If you only need it once, you can probably get along with the methods 1-4. However, if you are consuming multiple Promise-based APIs in your add-on or browser integration, I suggest trying out my recent JsPromise helper class from Viritin.

All the examples mentioned are available on GitHub.

Matti Tahvonen
Matti Tahvonen
Matti Tahvonen has a long history in Vaadin R&D: developing the core framework from the dark ages of pure JS client side to the GWT era and creating number of official and unofficial Vaadin add-ons. His current responsibility is to keep you up to date with latest and greatest Vaadin related technologies. You can follow him on Twitter – @MattiTahvonen
Other posts by Matti Tahvonen