Celery progress bar in React
Tags: react, django, javascript, redux
Published: 2021-02-12 04:07:55

[1. Overview](#1)
[2. Backend setup](#2)
[3. Frontend setup](#3)

# <a name="#1"></a> 1. Overview

The progress bar is one of the most useful UI components for tracking the actual progress of the task. But It is still a complex task to track the exact progress. In this tutorial, I will guide you to make the progress bar using [`celery-progress`](https://github.com/czue/celery-progress) library with react. 

> Please check [Celery Progress Bars for Django](https://github.com/czue/celery-progress) before starting this tutorial.

In this tutorial, I am going to upload the data to the server using Django Rest Framework (DRF), preprocess the data in the server, and send back the response. In my case, the preprocessing data will take 2-3 min, that's why I will visualize the progress bar of the processing in the frontend using react. 

# <a name="#2"></a> 2. Backend setup
I consider you already set up your [Django with celery](https://docs.celeryproject.org/en/stable/django/first-steps-with-django.html). Now you need to install the `celery-progress` library using pip,

```bash
pip install celery-progress
```

Add the endpoint URL to the celery-progress in `urls.py` file,

```python
from django.urls import re_path, include
re_path(r'^celery-progress/', include('celery_progress.urls')),
```

I am just writing the example function as `celery_function`. You need to replace the actual progress tracker function. 

```python
from celery import shared_task
from celery_progress.backend import ProgressRecorder
import time

@shared_task(bind=True)
def celery_function(self, seconds):
    progress_recorder = ProgressRecorder(self)
    result = 0
    for i in range(seconds):
        time.sleep(1)
        result += i
        progress_recorder.set_progress(i + 1, seconds)
    return result
```

I have a `Data` model like following,
```python
class Data(models.Model):
    name = models.CharField(max_length=100)
    taskId = models.CharField(max_length=200, blank=True)
    ...
    ...
    ...
```

Now, lets overwrite the `create` method in your `ViewSet` class of DRF and call the `celery_function.delay(time)` function. 

> In my code, my model name is `Data`, the serializer class is `DataSerializer`. I used the `patch` method to append the task_id to the Data model.

```python
class DataViewSet(viewsets.ModelViewSet):
    queryset = Data.objects.all()
    serializer_class = DataSerializer
    permission_classes = [permissions.IsAuthenticated]

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)

        if serializer.is_valid():
            data = serializer.data
            id = data['id']

            task = celery_function.delay(10)

            self.patch(id, {"taskId": task.id})

        return Response({'task_id': task.id}, status=status.HTTP_201_CREATED)

    def patch(self, pk, taskid):
        instance = self.get_object(pk)
        serializer = DataSerializer(
            instance, data=taskid, partial=True)

        if serializer.is_valid():
            serializer.save()
```
We set up everything in the backend. The task progress will be available on this URL: http://localhost:8000/celery-progress/${task_id}.

# <a name="#3"></a> 3. Frontend Setup

In the frontend, I am going to write the custom progress bar using `react-bootstrap`. The DRF backend will provide us the task progress in this URL: http://localhost:8000/celery-progress/${task_id}. Now we need to recursively hit this URL until the task status changes to complete. For this let's fetch the progress using [`axios`](https://github.com/axios/axios).

For this tutorial, I am using [react-redux](https://react-redux.js.org/). In the redux `actions.js` file, I have created two functions, one for adding the data (named as `addData`), and another for getting the progress of the task (named as `getProgress`). The `addData` function will only useful for adding the data to the server.

```js
export const addData = (data) => (dispatch, getState) => {
  axios
    .post(`http://localhost:8000/api/data/`, data)
    .then((res) => {
      dispatch({
        type: ADD_DATA,
        payload: res.data,
      });

      const task_id = res.data?.task_id;
      dispatch(getProgress(task_id));
    })
    .catch((err) =>
      console.log(err)
    );
};
```
The `getProgress` will take the actual progress of the task in with a `JSON` response. The example of the json response is as below,

```js
{"complete": true, "success": true, "progress": {"pending": false, "current": 100, "total": 100, "percent": 100}, "result": "Done"}
```

Here, I recursively call the `getProgress` function to get the current progress until the task gets completed. 

```js
export const getProgress = (taskId) => {
  return (dispatch) => {
    return axios
      .get(`http://localhost:8000/celery-progress/${taskId}`)
      .then((res) => {
        dispatch({
          type: PROGRESS_BAR,
          payload: { taskid: taskId, ...res.data },
        });
        if (!res.data.complete && !res.data?.progess?.pending) {
          return dispatch(getProgress(taskId));
        }
      })
      .catch((err) =>
        console.log(err)
      );
  };
};
```

Also in the redux `reducers.js` file, I added the response as follows,

```js
import { ADD_DATA, PROGRESS_BAR } from "../actions/types";

const initialState = {
  progress: {},
  data: [],
};

export default function (state = initialState, action) {
  switch (action.type) {
    case PROGRESS_BAR:
      return {
        ...state,
        progress: action.payload,
      };

    case ADD_DATA:
      return {
        ...state,
        data: action.payload,
      };
    default:
      return state;
  }
}
```
Now, let's write the react component to visualize the progress bar, as below,

```js
import React, { Component } from "react";
import { connect } from "react-redux";
import { ProgressBar } from "react-bootstrap";

class ProgressBarUi extends Component {
  render() {
    const { percent } = this.props;
    return (
      <ProgressBar
        now={percent}
        animated
        variant="success"
        label={`${percent}%`}
      />
    );
  }
}

export default ProgressBarUi;
```

The progress bar needs to visualize only when the progress is not completed, and not pending,

```js
import React, { Component } from "react";
import { connect } from "react-redux";
import { addData } from "../../../actions";
import ProgressBarUi from "../../common/ProgressBar";

class AddData extends Component {
  onSubmit = (e) => {
    const data = {
      key1: "value1",
      key2: "value2",
    };
    this.props.addExposure(data);
  };

  render() {
    const { progress } = this.props;

    return (
      <div>
        {/* your progress bar goes here  */}
        {progress?.progress && !progress?.complete && (
          <ProgressBarUi percent={progress.progress?.percent} />
        )}

        ...
        ...
        ...
        {/* data submit button */}
        <input type="submit" value="Add data" onSubmit={this.onSubmit} />
      </div>
    );
  }
}

const mapStateToProps = (state) => ({
  progress: state.progress,
});

export default connect(mapStateToProps, {
  addData,
})(AddData);
```
Alright, finally you successfully set up the progress bar using Django, react, and celery.