Skip to main content

To-Do list app using React

In this tutorial we will create a To-Do List app using ActiveJ and React. We will explore how to integrate React in an ActiveJ project and how to simply manage routing using the HTTP module. You can find full example sources on GitHub

Here we will consider only the main app class - ApplicationLauncher. It uses ActiveJ HttpServerLauncher and AsyncServlet classes for setting up an embedded application server. With this approach, you can create servers with no XML configurations or third-party dependencies. Moreover, HttpServerLauncher will automatically take care of launching, running and stopping the application. You'll only need to provide launcher with servlets.

Creating Launcher

ApplicationLauncher is the main class of the program. Besides launching the application, it also handles routing and most of the corresponding logic. We will use ActiveJ HttpServerLauncher and extend it:

public final class ApplicationLauncher extends HttpServerLauncher {
@Provides
RecordDAO recordRepo() {
return new RecordImplDAO();
}

@Provides
Executor executor() {
return newSingleThreadExecutor();
}

First, we provided RecordDAO with the application business logic and Executor which is needed for our AsyncServlet. In order to transform objects to/from JSON we chose DSL-JSON library, so we provide a DslJson<?> object that will handle data marshalling. AsyncServlet loads static content from /build directory and takes care of the routing:

@Provides
IStaticLoader staticLoader(Reactor reactor, Executor executor) {
return IStaticLoader.ofClassPath(reactor, executor, "build/");
}

@Provides
JsonCodec<Record> recordJsonCodec() {
return JsonCodecs.ofObject(Record::new,
"title", Record::getTitle, JsonCodecs.ofString(),
"plans", Record::getPlans, JsonCodecs.ofList(
JsonCodecs.ofObject(Plan::new,
"text", Plan::getText, JsonCodecs.ofString(),
"isComplete", Plan::isComplete, JsonCodecs.ofBoolean())
));
}

@Provides
AsyncServlet servlet(Reactor reactor, IStaticLoader staticLoader, RecordDAO recordDAO, JsonCodec<Record> recordJsonCodec) {
JsonCodec<Map<Integer, Record>> mapJsonCodec = JsonCodecs.ofMap(JsonKeyCodec.ofNumberKey(Integer.class), recordJsonCodec);
return RoutingServlet.builder(reactor)
.with("/*", StaticServlet.builder(reactor, staticLoader)
.withIndexHtml()
.build())

Routing in ActiveJ HTTP module resembles Express approach. Method with(@Nullable HttpMethod method, String path, AsyncServlet servlet) of RoutingServlet's builder adds routes to the RoutingServlet:

  • method (optional) is one of the HTTP methods (GET, POST, etc.)
  • path is the path on the server
  • servlet defines the logic of request processing. If you need to get some data from the request while processing you can use:
    • request.getPathParameter(String key)/request.getQueryParameter(String key) (see example of query parameter usage to provide the key of the needed parameter and receive back a corresponding String
    • request.getPostParameters() to get a Map of all request parameters

Pay attention to the * in the provided request. It states that whichever path until the next / is received, it will be processed by our static servlet, which uploads static content from /build directory.

Servlet should be able to add a new record, get all available records, delete record by its id and also mark plans of particular record as completed or not, so we provide corresponding routing:

.with(POST, "/add", request -> request.loadBody()
.then($ -> {
ByteBuf body = request.getBody();
try {
byte[] bodyBytes = body.getArray();
Record record = JsonUtils.fromJsonBytes(recordJsonCodec, bodyBytes);
recordDAO.add(record);
return HttpResponse.ok200().toPromise();
} catch (MalformedDataException e) {
return HttpResponse.ofCode(400).toPromise();
}
}))
.with(GET, "/get/all", request -> {
Map<Integer, Record> records = recordDAO.findAll();
return HttpResponse.ok200()
.withJson(JsonUtils.toJson(mapJsonCodec, records))
.toPromise();
})
//[START REGION_4]
.with(GET, "/delete/:recordId", request -> {
int id = parseInt(request.getPathParameter("recordId"));
recordDAO.delete(id);
return HttpResponse.ok200().toPromise();
})
//[END REGION_4]
.with(GET, "/toggle/:recordId/:planId", request -> {
int id = parseInt(request.getPathParameter("recordId"));
int planId = parseInt(request.getPathParameter("planId"));

Record record = recordDAO.find(id);
Plan plan = record.getPlans().get(planId);
plan.toggle();
return HttpResponse.ok200().toPromise();
})
.build();

Pay attention to the requests with :, for example:

.with(GET, "/delete/:recordId", request -> {
int id = parseInt(request.getPathParameter("recordId"));
recordDAO.delete(id);
return HttpResponse.ok200().toPromise();
})

: states that the following characters until the next / is a variable whose keyword, in this case, is recordId.

Running the application

To run the example clone ActiveJ and import it as a Maven project. Check out branch v6.0-beta2. Before running the example, build the project (Ctrl + F9 for IntelliJ IDEA).

Then, run the following command in activej/examples/tutorials/react-integration-2/front directory in terminal:

npm i
npm run-script build

Open ApplicationLauncher and run its main method. Then open your favourite browser and go to localhost:8080. Try to add and delete some tasks or mark them as completed