In this post you will learn how to implement a progress bar with Server-Sent Events, Spring and React

Introduction

At my daily work I was faced with the challenge to write a progess bar for an file upload scenario. I began a little research and stumbled upon Server Send Events (SSE). I found a lot of material describing SSE and showing a backend in several languages, but non of it where showing an actual front- and backend scenario. That's the reason I wanted to write this article.

Before heading to code, let's first look at SSE and what they actually are. If you are only interested in the implementation, feel free to skip this part.

Server-Sent Events

From the specification of the official w3c working draft of December 22th in 2009 Server-Sent Events can be described as an API for opening HTTP connection for receiving push notifications from a server in the form of DOM events [1].

The specification consists of four methods:

  • onopen
  • onmessage
  • onerror
  • close

The data that is sent by SSE is always string based, so we can also use JSON serialization and deserialization.

With this capabilities it is possible to use SSE in various scenarios for different use cases. One of this scenarios could be to write a progress bar, which will be shown in the next chapters.

Backend implementation with Spring

The framework Spring is supporting Server-Sent Events since the version 4.2, but since Spring version 5, it's handling and implementation has been improved significantly. In order to use SSE we do not need an extra dependency, it's already included into spring-boot-start-web :

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

To work with SSE we need a controller class. It must be annotated as always, so we need a @RestController and at least one @GetMapping. We want to test our code locally, so we also need the @CrossOrigin("*"). To sum it all up, here is an example controller with an sse endpoint:

@RestController
@RequestMapping("/progress")
@CrossOrigin("*")
public class ProgressbarController {
private final Gson gson;
public ProgressbarController() {
this.gson = new Gson();
}
@GetMapping("/progressbar")
public ResponseEntity<SseEmitter> getAndCreateProgress(){
SseEmitter sseEmitter = new SseEmitter();
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> this.createAndGetSequence(sseEmitter));
return new ResponseEntity<>(sseEmitter, HttpStatus.OK);
}
private void createAndGetSequence(SseEmitter sseEmitter) {
for (int i = 1; i <= 10 ; i++) {
try {
sseEmitter.send(gson.toJson(new ProgressResponseModel(i, 10)).replaceAll("\n", ""));
Thread.sleep(500);
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
sseEmitter.complete();
}
}

The first thing you will probably notice is the gson instance variable.

The reason for it is, that the SSE payload is string-based and we want to send json to the frontend, therefore we can use any serializer we want. I personally prefer gson over the default jackson json converter from Spring. This is why we have to create an instance of it created inside the constructor.

The only endpoint getAndCreateProgress contains the actual implementation for SSE to work. We have to return an instance of SseEmitter, which is the Spring implementation of Server-Sent Events. It contains various methods to fulfill different requirements. In this tutorial the important once are send(Object object) and complete().

To use it, we need to create an instance of SseEmitter and because we don't want to block the actual thread that is passing the SseEmitter to the frontend, we need to start a new thread. For this we use the ExecutorService. An important note here is, that is highly depends on the use case, if we want to use a singleThreadExecutor or an other variation of it. This is also true for the initialization of the SseEmitter inside the endpoint itself.

With things now prepared, we pass our mock sequence to the executor's execute method. Inside the createAndGetSequence method we have a loop, which counts to 10 and sends an ProgressResponseModel with each step. We also replace all the "\n" created by the the deserialization of Gson, because otherwise they would be interpreted as completely new messages.

To better visualize the progress we also have a little sleep timer inside the for loop. After the loop is done, we use the complete method of the SseEmitter to close the connection.

The model class, which is serialized and send by the SseEmitter looks as follows:

@AllArgsConstructor
@Data
public class ProgressResponseModel {
private final int count;
private final int maxCount;
}

Note, that I use Lombok here to save us from too many boilerplate code.

For the demo purpose of this backend this is actually enough, we don't need to implement more.

If you want to test your backend at this state, you can use a HTTP-Client like Postman. Simply execute a GET-Request to the specified endpoint above and Postman does the rest for you. Of course you could and should use the test framework of Spring to test your code.

Frontend implementation with React

I won't show you how to setup a blank React project, if you are not familiar with it, then you can visit React Homepage.

As soon as we have setup the project, we need some additional dependencies here. The first bunch of dependencies are the material-ui, because it already consists of a base progress bar component, so we don't need to implement it ourselves:

npm install @mui/material @emotion/react @emotion/styled --save

The second dependency is a better API for fetching Server-Sent Events:

npm i @microsoft/fetch-event-source --save

This library provides a better API for making Event Source requests - also known as server-sent events - with all the features available in the Fetch API [2]. With this library we can even send jwt tokens or other custom headers to our backend, which is not possible with the base implementation of EventSource.

Let's first look at our implementation of the Progressbar itself, inside the Progressbar.tsx file:

interface IProps {
progress: number
}
const Progressbar: React.FC<IProps> = (props: IProps) => {
return(
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant="determinate" value={props.progress}/>
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${Math.round(
props.progress,
)}%`}</Typography>
</Box>
</Box>
)
}
export default Progressbar;

Here we are just using the material-ui. The only thing we want to add here, is the progress property, which is used by the progress bar to show the current progress.

The corresponding SSE logic of the frontend lays iniside the ProgressContext.tsx. Of course, you don't have to write a React context, but I like the concept of separating the actual logic from the view:

interface IProgressContext {
progress: number,
startProcess(): void
}
interface IProps {
children: React.ReactNode
}
interface IProgressState {
count: number,
maxCount: number
}
export const ProgressContext = createContext<IProgressContext>({
progress: 0,
startProcess: () => {}
});
const ProgressProvider: React.FC<IProps> = ({children}) => {
const [progress, setProgress] = useState<number>(0);
const startProcess = useCallback(() => {
// url should be inside an environmental variable.
fetchEventSource('http://localhost:8080/progress/progressbar', {
onmessage(ev: EventSourceMessage){
const progressState: IProgressState = JSON.parse(ev.data)
setProgress(progressState.count / progressState.maxCount * 100)
if(progressState.maxCount === progressState.count){
alert('Process was finished successfully')
setTimeout(() => {
setProgress(0)
}, 1500)
}
}
})
}, [])
return <ProgressContext.Provider value={{ progress: progress, startProcess: startProcess }}>{children}</ProgressContext.Provider>
}
export default ProgressProvider;

The frist two interfaces are for the context itself and it's types. The third one, IProgressState, is a direct representation of our model we created inside the Spring project. We also instantiate a progress state as number type inside our Provider. The Provider consists of one method.

The startProcess method connects to the Spring endpoint. With the connection established, we can overwrite the onmessage method from the fetchEventSource and implement our logic. We parse our json data to our IProgressState.

Now we can use it, to set our state with the setProgress method. We calculate the percentage with dividing the current count by the maximum count and multiply the result with 100. We also want to reset our progress if it reaches 100%, this is implemented by the if condition. It just gives us an alert and sets the progress to zero after 1.5 seconds.

Now we can connect it all together in our ProgressbarDemo.tsx file:

[...]
<ProgressProvider>
<ProgressbarDemo/>
</ProgressProvider>
[...]

Don't forget to use the provider inside the App.tsx.

If you boot up the backend and frontend locally, the result should look as follows:

progress_bar_animated_77b986b88b.gif

Summary

Thank you for reading this article. I hope you have a better understanding of Sever-Side Events and it's implementation now. Perhaps you have been inspired and can use it for your current scenario.

If you encounter any problems or you have another question, feel free to use our contact form or write us a message directly!

As always the code is available on GitHub.

References

  1. Server-Sent Events W3C Working Draft 22 December 2009 - w3.org 27.08.2023
  2. Fetch Event Source - github.com 27.08.2023