Siirry pääsisältöön
← Blog

Real-time UI updates in Java: push changes from the server

· 8 minuuttia
Hyyan Abo Fakher
Lead Software Architect

cover image

In webforJ you get real-time UI updates in Java by changing components from the server and letting the framework carry the change to the browser. For work that runs outside the UI thread, you wrap the change in Environment.runLater(). For recurring refreshes you use Interval, and for typing you use Debouncer. You write no WebSocket or server-sent events code.

This post builds two small pieces: a progress panel fed by a background job, and a dashboard number that refreshes itself. Along the way it covers the one rule that decides whether any of this works, which is which thread may update a component.

How does a Java web app push updates to the browser?​

In a webforJ app the components live on the server. When you call setText() on a Paragraph or setValue() on a ProgressBar, the browser view changes. There is no endpoint to poll and no client script that listens for messages, because you never wrote the layer between them.

That makes "push" a question about threads rather than protocols. A server-side change reaches the user as soon as it runs on the right thread. Everything below is about getting your code onto that thread at the right time.

Which thread can update components?​

webforJ enforces a strict threading model. All UI operations must happen on the Environment thread, the thread bound to the user's session. There are three reasons, covered in the asynchronous updates guide: the underlying webforJ API binds to the thread that created the session, components keep state that isn't thread-safe, and UI events are processed sequentially on a single thread.

Event handlers already run there, so a click listener can update components directly. The rule bites when you start your own work. A background thread that changes a component directly fails, because it isn't the session's UI thread. Every UI update from a background thread goes through Environment.runLater(), with no exceptions.

How do you update the UI from a background job?​

Environment.runLater() schedules a task on the UI thread of one user. Its first argument is that user's Environment, which you read with Environment.getCurrent() while you are still on the UI thread. It comes in two forms, one taking a Runnable and one taking a Supplier, and both return a PendingResult that tracks completion and lets you cancel the task. The asynchronous updates guide covers both forms.

Here is a panel that runs a long import on an executor and reports progress as it goes:

@Route("jobs")
public class JobPanel extends Composite<Div> {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final List<PendingResult<?>> updates = new ArrayList<>();
private final ProgressBar progress = new ProgressBar();
private final Paragraph status = new Paragraph("Idle");
private final Button start = new Button("Start import", ButtonTheme.PRIMARY);

public JobPanel() {
start.onClick(e -> {
start.setEnabled(false);
Environment env = Environment.getCurrent();
executor.submit(() -> runImport(env));
});
getBoundComponent().add(start, progress, status);
}

private void runImport(Environment env) {
for (int i = 1; i <= 100; i++) {
int percent = i;
importRecord(percent);
updates.add(Environment.runLater(env, () -> {
progress.setValue(percent);
status.setText("Imported " + percent + " of 100 records");
}));
}
Environment.runLater(env, () -> {
status.setText("Import finished");
start.setEnabled(true);
});
}
}

The click handler runs on the user's UI thread, so that is where Environment.getCurrent() returns the right Environment. The handler passes it to the import, and the import passes it to every runLater() call. Tasks submitted from a background thread are queued and run in strict first-in, first-out order, so the user never sees progress jump backwards.

Which thread may call runLater?​

Any thread, as long as it has the target Environment in hand. A static initializer, a java.util.Timer thread or a callback from an HTTP client's async methods all work the same way as the executor in the panel. What none of them can do is read Environment.getCurrent(), because that returns null off the UI thread. Read it in the handler or the constructor, store it in a local or a field, and pass it along.

Cleaning up when the user leaves​

Queued tasks hold references to the components they update. Cancel them when the view is destroyed so a finished navigation doesn't keep the view alive:

@Override
protected void onDestroy() {
updates.forEach(u -> {
if (!u.isDone()) {
u.cancel();
}
});
executor.shutdownNow();
}

Cancellation only stops tasks that haven't started. One that is already running completes.

How do you refresh a number on a timer?​

For a dashboard figure that should stay current, use Interval. It is a timer with a fixed delay in seconds that fires an event each time it elapses. It has existed since 24.02 and requires no thread management on your side.

@Route("dash")
public class Dashboard extends Composite<Div> {
private final Paragraph orders = new Paragraph("0");
private final Interval refresh;

public Dashboard() {
refresh = new Interval(5f, e -> orders.setText(String.valueOf(orderCount())));
refresh.start();
getBoundComponent().add(orders);
}

@Override
protected void onDidDestroy() {
refresh.stop();
}
}

An Interval needs start() before it fires, and stop() and restart() control it afterwards. You can change the delay with setDelay(), which takes effect after a stop or restart. Very small delays flood the app with events faster than it can handle them.

Prefer Interval over a java.util.Timer or a thread per user. A web app has many concurrent users, and per-user threads run out quickly.

How do you debounce search as the user types?​

Sending a query on every keystroke wastes work. Debouncer waits until the user stops typing. It is available since 25.11, takes its delay in seconds, and each new call resets the timer.

Debouncer debounce = new Debouncer(0.3f);

search.onModify(e -> debounce.run(() -> find(search.getText())));

The action runs on the UI thread, using Interval underneath, so you don't wrap it in runLater(). As with intervals, call debounce.cancel() when the component is destroyed. When you need a pending action to run immediately, for example before a form submit, flush() does that. The post on how to debounce input in Java goes through search as you type, cancel, and flush.

If you only want to delay an element event and need no cancel or flush, ElementEventOptions.setDebounce() does it in the browser instead, in milliseconds.

Can Spring's @Async drive the UI?​

Yes. With a Spring Boot app, a service method annotated with @Async runs on a Spring-managed background thread, and it can report progress through a callback:

@Service
public class ReportService {

@Async
public CompletableFuture<String> build(Consumer<Integer> progress) {
// long-running work that calls progress.accept(percent)
return CompletableFuture.completedFuture("done");
}
}

Add @EnableAsync to your app class, because without it @Async methods run synchronously. Inject the service into the view's constructor, read the Environment in the click handler, then wrap the callback body in Environment.runLater() with it before it updates a component. The same applies to whenComplete on the returned future. The Spring background jobs page walks through the full flow, including cancelling the future in onDestroy().

Which tool fits which job?​

NeedUse
Report progress from a long jobEnvironment.runLater() from the worker
Keep a figure fresh on a scheduleInterval
Wait for typing to pauseDebouncer
Run a Spring service off the UI thread@Async plus runLater() in callbacks

All four stay in Java and in the same threading model. The point of webforJ here is that the server decides what the user sees and the framework handles delivery, which removes the message formats, reconnect logic and client listeners you would otherwise own. For the data side, how a webforJ UI reacts to data changes covers events, binding, and repository commits.

To try the patterns, generate a project with startforJ and start from the asynchronous updates guide.

Frequently asked questions​

Do I need WebSockets or SSE to update a webforJ page?​

No. You change components in Java and the framework delivers the change to the browser. The work you do is choosing the right thread, which Environment.runLater() handles.

Why does Environment.runLater take an Environment?​

A background thread belongs to no user, so it can't tell which session's UI to update. The argument names that session. Read it with Environment.getCurrent() on the UI thread, in a handler or a constructor, and pass it to the work. On a background thread getCurrent() returns null.

Does Environment.runLater keep updates in order?​

Yes. Tasks submitted from a background thread run in first-in, first-out order on the UI thread. Each call returns a PendingResult, so you can cancel tasks that haven't started when the view is destroyed.

Should I use Interval or a Java Timer?​

Use Interval. It needs no extra configuration, runs inside the webforJ threading model, and avoids creating a timer or thread per user.

Rakennatko jotain webforJ:llä?

Keskustele tiimin kanssa projektistasi, migraatiosta tai siitä, sopiiko webforJ sinulle.