- 1. Introduction
- 2. Prerequisites
- 3. Quick Start
- 4. Project Structure
- 5. Architecture
- 6. Java API: Functional Components (Approach A)
- 7. Java API: Builder DSL (Approach B)
- 8. Java API: Class-Based Components (Approach C)
- 9. Kotlin DSL
teavm-react is a library that lets you write React 18 applications in Java and Kotlin, compiled to JavaScript via TeaVM.
You get full React capabilities — components, hooks, context, memo, refs — with complete type safety, IDE support, and zero JavaScript knowledge required.
| Module | Purpose |
|---|---|
|
Java API bindings to React 18: hooks, HTML DSL, builder DSL, class-based components, events, context. |
|
Idiomatic Kotlin wrapper: delegated state properties, lambda-with-receiver HTML builder, |
|
Kitchen-sink demo application exercising every library feature. |
<groupId>ca.weblite</groupId>
<artifactId>teavm-react-core</artifactId>
<version>0.1.0-SNAPSHOT</version>| React (JavaScript) | teavm-react (Java) | teavm-react (Kotlin) |
|---|---|---|
|
|
|
|
|
|
|
|
|
-
JDK 21 (build-time; source/target compatibility is Java 11)
-
Maven 3.8+
-
A modern web browser (React 18 is loaded via CDN at runtime)
-
For Kotlin development: no extra tooling needed (the
kotlin-maven-pluginis configured in the project)
package com.example;
import ca.weblite.teavmreact.core.React;
import ca.weblite.teavmreact.core.ReactDOM;
import ca.weblite.teavmreact.core.ReactElement;
import ca.weblite.teavmreact.hooks.Hooks;
import ca.weblite.teavmreact.hooks.StateHandle;
import org.teavm.jso.JSObject;
import org.teavm.jso.dom.html.HTMLDocument;
import static ca.weblite.teavmreact.html.Html.*;
public class App {
public static void main(String[] args) {
var root = ReactDOM.createRoot(
HTMLDocument.current().getElementById("root"));
root.render(component(App::render, "App"));
}
static ReactElement render(JSObject props) {
StateHandle<Integer> count = Hooks.useState(0);
return div(
h1("Hello teavm-react!"),
p("Count: " + count.getInt()),
button("Increment")
.onClick(e -> count.updateInt(n -> n + 1))
.build()
);
}
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My App</title>
</head>
<body>
<div id="root"></div>
<!-- React 18 from CDN — MUST load before classes.js -->
<script src="https://unpkg.com/react@18/umd/react.development.js"
crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
crossorigin></script>
<!-- TeaVM-compiled output -->
<script src="js/classes.js"></script>
<script>main([]);</script>
</body>
</html>mvn clean install -N # install parent POM
mvn install -pl teavm-react-core -DskipTests # install core
mvn process-classes -pl your-app # TeaVM compilationOr use the provided convenience script for the demo:
./run.sh # builds and serves on http://localhost:8080
./run.sh 3000 # custom portteavm-react/
├── pom.xml # Parent POM (dependency management)
├── run.sh # Build & serve script
├── teavm-react-core/ # Java API library
│ └── src/main/java/ca/weblite/teavmreact/
│ ├── core/ # React, ReactDOM, ReactElement,
│ │ # ReactRoot, ReactContext, RenderFunction
│ ├── hooks/ # Hooks, StateHandle, RefHandle,
│ │ # EffectCallback
│ ├── events/ # EventHandler, ChangeEvent,
│ │ # KeyboardEvent, MouseEvent, etc.
│ ├── html/ # Html, ElementBuilder, DomBuilder
│ └── component/ # ReactView (class-based components)
├── teavm-react-kotlin/ # Kotlin DSL library
│ └── src/main/kotlin/ca/weblite/teavmreact/kotlin/
│ ├── ComponentScope.kt # fc() and ComponentScope
│ ├── HtmlBuilder.kt # Lambda-with-receiver HTML DSL
│ ├── StateDelegate.kt # Delegated state properties
│ ├── RefDelegate.kt # Delegated ref properties
│ ├── EffectScope.kt # Effect hooks with coroutines
│ ├── FlowExtensions.kt # Flow.collectAsState()
│ ├── StyleBuilder.kt # Type-safe inline styles
│ ├── TypedContext.kt # Typed React context
│ ├── JsDispatcher.kt # JS main-thread coroutine dispatcher
│ └── Util.kt # depsToJsArray, encoding helpers
└── teavm-react-demo/ # Kitchen-sink demo app
└── src/main/
├── java/.../demo/App.java # Demo entry point (727 lines)
└── webapp/
└── index.html # HTML shell with React CDNJava / Kotlin source
│
▼
Maven compile (javac / kotlinc)
│
▼
TeaVM compiler (teavm-maven-plugin)
│
▼
classes.js (JavaScript output)
│
▼
Browser loads index.html
├── React 18 (from CDN)
└── classes.js
│
▼
main([]) called → ReactDOM.createRoot() → React rendersteavm-react bridges Java and JavaScript using three TeaVM annotations:
Embeds raw JavaScript inside a Java method. This is how all React API calls are made.
@JSBody(params = {"type", "props"}, script =
"return React.createElement(type, props);")
public static native ReactElement createElement(String type, JSObject props);Marks a functional interface so TeaVM passes its implementation as a raw JavaScript function (not a Java wrapper object).
@JSFunctor
public interface EventHandler extends JSObject {
void handleEvent(SyntheticEvent event);
}|
Important
|
No ES6 spread in // Instead of: React.createElement(type, props, ...children)
@JSBody(params = {"type", "props", "children"}, script =
"return React.createElement.apply(null, " +
"[type, props].concat(Array.prototype.slice.call(children)));") |
|
Important
|
Event handlers must use dedicated setter methods: Passing a // CORRECT — raw JS function is passed
React.setOnClick(props, handler);
// WRONG — would wrap the function in a Java object
React.setProperty(props, "onClick", handler); // DO NOT DO THIS |
The functional approach mirrors idiomatic React.js. You define components as static methods, use Html. static imports for elements, and call Hooks. for state and effects.
A component is any method matching the RenderFunction signature: it takes a JSObject props parameter and returns a ReactElement. Register it with React.wrapComponent():
import static ca.weblite.teavmreact.html.Html.*;
JSObject myComponent = React.wrapComponent(App::myRender, "MyComponent");
static ReactElement myRender(JSObject props) {
return div(
h1("Hello World"),
p("This is a functional component.")
);
}static ReactElement counter(JSObject props) {
StateHandle<Integer> count = Hooks.useState(0);
return div(
h2("Count: " + count.getInt()),
button("Increment")
.onClick(e -> count.updateInt(n -> n + 1))
.build(),
button("Reset")
.onClick(e -> count.setInt(0))
.build()
);
}StateHandle<T> provides typed getters and setters:
-
getInt(),setInt(int),updateInt(IntUpdater) -
getString(),setString(String),updateString(StringUpdater) -
getBool(),setBool(boolean) -
getDouble(),setDouble(double) -
getDouble(),setDouble(double)
static ReactElement timer(JSObject props) {
StateHandle<Integer> seconds = Hooks.useState(0);
// Run once on mount (empty deps), clean up on unmount
Hooks.useEffect(() -> {
var id = setInterval(() -> seconds.updateInt(n -> n + 1), 1000);
return () -> clearInterval(id); // cleanup
}, Hooks.deps()); // empty array = mount only
return p("Elapsed: " + seconds.getInt() + "s");
}-
Hooks.useEffect(callback)— runs after every render -
Hooks.useEffect(callback, Hooks.deps())— runs once on mount -
Hooks.useEffect(callback, Hooks.deps(dep1, dep2))— runs when deps change
The callback returns a VoidCallback for cleanup, or null for no cleanup.
Html.button(), Html.input(), Html.a(), Html.textarea(), Html.select(), and Html.img() return an ElementBuilder for fluent prop chaining. Call .build() to finalize:
// Button with click handler
button("Click me")
.onClick(e -> doSomething())
.className("btn-primary")
.disabled(false)
.build()
// Text input with controlled value
input("text")
.value(name.getString())
.onChange(e -> name.setString(e.getTarget().getValue()))
.placeholder("Enter name")
.build()
// Link
a("Visit site")
.href("https://example.com")
.target("_blank")
.build()// Register a component once (typically as a static field)
static final JSObject counter = React.wrapComponent(App::counterRender, "Counter");
// Render it as a child element
static ReactElement app(JSObject props) {
return div(
h1("My App"),
Html.component(counter), // no props
Html.component(counter, myProps) // with props
);
}List<String> items = List.of("Apple", "Banana", "Cherry");
return ul(
Html.mapToElements(items, item -> li(item))
);The builder DSL provides a Java-idiomatic fluent pattern using DomBuilder inner classes. Each HTML element type has a dedicated class with a create() factory method.
import ca.weblite.teavmreact.html.DomBuilder.*;
ReactElement view = Div.create()
.className("container")
.child(H1.create().text("Hello World").build())
.child(P.create().text("Built with the builder DSL.").build())
.child(Button.create()
.text("Click me")
.onClick(e -> count.updateInt(n -> n + 1))
.build())
.build();Layout: Div, Span, Section, Article, Aside, Header, Footer, Main, Nav
Headings: H1, H2, H3, H4, H5, H6
Text: P, Pre, Code, Blockquote, Em, Strong, Small
Lists: Ul, Ol, Li, Dl, Dt, Dd
Table: Table, Thead, Tbody, Tfoot, Tr, Th, Td, Caption
Form: Form, Fieldset, Legend, Label, Button, Input, Select, Textarea
Media: A, Img, Figure, Figcaption
Misc: Hr, Br, Details, Summary
The Input class provides typed factory methods:
Input.text() // <input type="text">
Input.password() // <input type="password">
Input.checkbox() // <input type="checkbox">
Input.number() // <input type="number">All builders inherit from DomBuilder:
-
.text(String)— set text content -
.child(ReactElement)/.child(DomBuilder)/.child(ElementBuilder)— add children -
.className(String)/.id(String)/.key(String|int) -
.style(Style)— inline styles (seeStyle.create()) -
.onClick(EventHandler)/.onChange(ChangeEventHandler)/.onKeyDown(KeyboardEventHandler) -
.onFocus(FocusEventHandler)/.onBlur(FocusEventHandler)/.onSubmit(SubmitEventHandler) -
.value(String)/.placeholder(String)/.disabled(boolean)/.type(String) -
.href(String)/.src(String)/.alt(String) -
.prop(String, String|int|boolean|double)— arbitrary property -
.forEach(List, mapper)/.forEachElement(List, mapper)— list iteration -
.build()— finalize the element
static ReactElement formComponent(JSObject props) {
StateHandle<String> name = Hooks.useState("");
StateHandle<String> email = Hooks.useState("");
return Form.create()
.className("form")
.child(Div.create()
.child(Label.create().text("Name:").build())
.child(Input.text()
.value(name.getString())
.onChange(e -> name.setString(e.getTarget().getValue()))
.placeholder("Your name")
.build())
.build())
.child(Div.create()
.child(Label.create().text("Email:").build())
.child(Input.text()
.value(email.getString())
.onChange(e -> email.setString(e.getTarget().getValue()))
.placeholder("your@email.com")
.build())
.build())
.child(Button.create()
.text("Submit")
.onClick(e -> {
// handle submission
})
.build())
.build();
}The class-based approach is familiar to Swing/JavaFX developers. Extend ReactView, override render(), and declare state as field initializers.
public class CounterView extends ReactView {
private final StateHandle<Integer> count = Hooks.useState(0);
@Override
protected ReactElement render() {
return Html.div(
Html.h2("Count: " + count.getInt()),
Html.button("Increment")
.onClick(e -> count.updateInt(n -> n + 1))
.build()
);
}
}// As a ReactElement (most common)
ReactView.view(CounterView::new, "CounterView")Override onMount() and onUnmount() for setup and teardown:
public class StopwatchView extends ReactView {
private final StateHandle<Integer> elapsed = Hooks.useState(0);
private final StateHandle<Boolean> running = Hooks.useState(false);
@Override
protected void onMount() {
// called once when the component mounts
}
@Override
protected void onUnmount() {
// called when the component unmounts — clean up resources
}
@Override
protected ReactElement render() {
return Html.div(
Html.h3("Stopwatch: " + elapsed.getInt() + "s"),
Html.button(running.getBool() ? "Stop" : "Start")
.onClick(e -> running.setBool(!running.getBool()))
.build()
);
}
}|
Warning
|
|
The teavm-react-kotlin module provides an idiomatic Kotlin experience using delegated properties, lambda-with-receiver builders, and coroutine integration.
Define components using fc(), which provides a ComponentScope receiver:
val Counter = fc("Counter") {
var count by state(0)
div {
h2 { +"Count: $count" }
button {
+"Increment"
onClick { count++ }
}
}
}Render at the app root:
fun main(args: Array<String>) {
val root = ReactDOM.createRoot(
HTMLDocument.current().getElementById("root"))
root.render(component(Counter))
}State is declared using Kotlin’s by delegation. Assignments trigger React re-renders:
var count by state(0) // IntStateDelegate
var name by state("Alice") // StringStateDelegate
var active by state(true) // BooleanStateDelegate
var price by state(9.99) // DoubleStateDelegate
var items by stateList("a", "b") // StringListStateDelegateFunctional updates are available via the delegate’s update() method:
val countDelegate = state(0)
var count by countDelegate
// Later, functional update:
countDelegate.update(StateHandle.IntUpdater { prev -> prev + 1 })The HtmlBuilder uses Kotlin’s lambda-with-receiver pattern for a natural HTML-like syntax:
div {
className("container")
h1 { +"Welcome" }
p { +"Some text content" }
ul {
li { +"Item 1" }
li { +"Item 2" }
li(key = "special") { +"Item 3" }
}
button {
+"Click me"
className("btn")
onClick { handleClick() }
}
input("text") {
value(name)
placeholder("Enter name")
onChange { e -> name = e.getTarget().getValue() }
}
}+"Hello" // add text node (String.unaryPlus)
+someElement // add child ReactElement
+SomeComponent // render a wrapped component as child
+listOfElements // splice a List<ReactElement>All standard HTML elements are available as nested builder functions: div, span, section, article, aside, header, footer, main, nav, h1-h6, p, pre, code, blockquote, em, strong, small, mark, sub, sup, label, ul, ol, li, dl, dt, dd, table, thead, tbody, tfoot, tr, th, td, caption, form, fieldset, legend, button, input, textarea, select, option, a, img, figure, figcaption, video, audio, source, details, summary, hr, br.
className("my-class")
id("my-id")
key("unique-key")
title("tooltip text")
tabIndex(0)
role("button")
draggable(true)
hidden(false)
prop("data-custom", "value") // arbitrary propsonClick { event -> ... }
onChange { event -> name = event.getTarget().getValue() }
onKeyDown { event -> if (event.getKey() == "Enter") submit() }
onKeyUp { event -> ... }
onFocus { event -> ... }
onBlur { event -> ... }
onSubmit { event -> ... }
onMouseDown { event -> ... }
onMouseUp { event -> ... }
onMouseEnter { event -> ... }
onMouseLeave { event -> ... }value("text")
defaultValue("initial")
placeholder("hint")
disabled(true)
readOnly(true)
checked(false)
type("password")
name("field-name")
htmlFor("input-id")
autoFocus(true)
maxLength(100)
rows(5)
cols(40)
href("https://example.com")
src("/image.png")
alt("description")
target("_blank")Use StyleBuilder for type-safe CSS-in-JS:
div {
style {
backgroundColor = "#282c34"
color = "white"
padding = "20px"
display = "flex"
flexDirection = "column"
gap = "10px"
borderRadius = "8px"
}
+"Styled content"
}Or use the css() shorthand to parse a CSS string:
div {
css("background-color: #282c34; color: white; padding: 20px")
+"Styled content"
}StyleBuilder provides typed properties for layout, flexbox, grid, box model, borders, colors, typography, and effects.
var renderCount by refInt(0) // Int ref (IntRefDelegate)
var label by refString("") // String ref (StringRefDelegate)Refs persist across renders without triggering re-renders (same as React’s useRef).
Memoize computed values that only recalculate when dependencies change:
val filtered = memo(items, filter) {
items.filter { it.contains(filter) }
}The effect() function wraps React’s useEffect and provides a CoroutineScope:
// Run after every render
effect {
println("rendered")
}
// Run once on mount (pass no dependencies explicitly)
effectOnce {
println("mounted")
onCleanup { println("unmounting") }
}
// Run when dependencies change
effect(userId) {
// re-runs when userId changes
onCleanup { /* cleanup previous */ }
}
// Launch coroutines inside effects
effect {
val job = launch {
while (isActive) {
delay(1000)
ticks++
}
}
onCleanup { job.cancel() }
}Collect a Kotlin Flow into React state:
val seconds by flow {
var t = 0
while (true) {
emit(t++)
delay(1000)
}
}.collectAsState(initial = 0)collectAsState is available for Flow<Int>, Flow<String>, Flow<Boolean>, and Flow<Double>. The flow starts collecting on mount and is automatically cancelled on unmount.
Type-safe React context without manual JSObject casting:
// Create typed contexts
val ThemeContext = createStringContext("light")
val CountContext = createIntContext(0)
val AuthContext = createBoolContext(false)
// Provide a value
ThemeContext.provide("dark") {
+App
}
// Consume in a component
val App = fc("App") {
val theme: String = useContext(ThemeContext)
div {
+"Current theme: $theme"
}
}Factory functions: createStringContext(), createIntContext(), createBoolContext().
Read props passed to a component:
val Greeting = fc("Greeting") {
val name = propString("name")
val age = propInt("age")
val active = propBool("active")
val data = propObj("data")
p { +"Hello, $name! Age: $age" }
}fragment {
h1 { +"Title" }
p { +"No wrapper element" }
}
== HTML Elements Reference
=== Functional DSL (`Html.*`)
All methods are static on `ca.weblite.teavmreact.html.Html`. Each element has two overloads: `element(String text)` and `element(ReactElement... children)`.
[cols="1,3"]
|===
|Category |Elements
|Layout
|`div`, `span`, `section`, `article`, `aside`, `header`, `footer`, `main`, `nav`
|Headings
|`h1`, `h2`, `h3`, `h4`, `h5`, `h6`
|Text
|`p`, `pre`, `code`, `blockquote`, `em`, `strong`, `small`, `sub`, `sup`, `mark`
|Lists
|`ul`, `ol`, `li`, `dl`, `dt`, `dd`
|Table
|`table`, `thead`, `tbody`, `tfoot`, `tr`, `th`, `td`, `caption`
|Form containers
|`form`, `fieldset`, `legend`, `label`
|Media
|`figure`, `figcaption`
|Void
|`hr()`, `br()` (no arguments)
|Details
|`details`, `summary`
|===
=== Interactive Elements (return `ElementBuilder`)
These return an `ElementBuilder` for fluent prop chaining. Call `.build()` to finalize.
[cols="1,2,2"]
|===
|Method |Signature |Notes
|`button`
|`button(String text)`
|Button with text label
|`input`
|`input(String type)`
|Input with type attribute
|`a`
|`a(String text)`
|Anchor with text
|`textarea`
|`textarea()`
|Textarea element
|`select`
|`select()`
|Select dropdown
|`img`
|`img()`
|Image (chain `.src()`, `.alt()`)
|===
=== `ElementBuilder` Methods
[cols="1,2"]
|===
|Method |Description
|`.className(String)`
|Set CSS class
|`.id(String)` / `.key(String\|int)`
|Set id or React key
|`.onClick(EventHandler)`
|Click handler
|`.onChange(ChangeEventHandler)`
|Change handler
|`.onKeyDown(KeyboardEventHandler)` / `.onKeyUp(...)`
|Keyboard handlers
|`.onFocus(FocusEventHandler)` / `.onBlur(...)`
|Focus handlers
|`.onSubmit(SubmitEventHandler)`
|Form submit handler
|`.onMouseDown(...)` / `.onMouseUp(...)` / `.onMouseEnter(...)` / `.onMouseLeave(...)`
|Mouse handlers
|`.value(String)` / `.placeholder(String)`
|Input value and placeholder
|`.disabled(boolean)` / `.checked(boolean)` / `.readOnly(boolean)`
|Input state attributes
|`.type(String)` / `.name(String)` / `.htmlFor(String)`
|Form attributes
|`.href(String)` / `.src(String)` / `.alt(String)` / `.target(String)`
|Link and media attributes
|`.rows(int)` / `.cols(int)` / `.maxLength(int)` / `.minLength(int)`
|Textarea/input sizing
|`.tabIndex(int)`
|Tab order
|`.style(Style)`
|Inline styles (see `Style.create()`)
|`.prop(String, String\|int\|boolean\|double)`
|Arbitrary property
|`.build()` / `.build(ReactElement...)`
|Finalize element
|===
=== Utility Methods
* `Html.text(String)` -- create a plain text node
* `Html.component(RenderFunction)` -- render an inline component with no props
* `Html.component(RenderFunction, String)` -- render with display name
* `Html.component(JSObject)` -- render a pre-wrapped component with no props
* `Html.fragment(ReactElement...)` -- wrap in `React.Fragment`
* `Html.mapToElements(List<T>, Function<T, ReactElement>)` -- map a list to elements
== Hooks Reference
All hooks are static methods on `ca.weblite.teavmreact.hooks.Hooks`. They *must* be called during a component render (inside a `RenderFunction`). Calling them outside a render violates React's Rules of Hooks.
=== `useState`
[cols="2,2"]
|===
|Signature |Returns
|`useState(int initial)`
|`StateHandle<Integer>`
|`useState(String initial)`
|`StateHandle<String>`
|`useState(boolean initial)`
|`StateHandle<Boolean>`
|`useState(double initial)`
|`StateHandle<Double>`
|===
==== `StateHandle<T>` API
*Getters:*
* `getInt()` -- current value as `int`
* `getString()` -- current value as `String`
* `getBool()` -- current value as `boolean`
* `getDouble()` -- current value as `double`
*Setters:*
* `setInt(int)`, `setString(String)`, `setBool(boolean)`, `setDouble(double)`
*Functional updates:*
* `updateInt(IntUpdater)` -- `setter(prev => updater(prev))`
* `updateString(StringUpdater)` -- `setter(prev => updater(prev))`
[source,java]@JSFunctor public interface IntUpdater extends JSObject { int update(int prev); }
@JSFunctor public interface StringUpdater extends JSObject { String update(String prev); }
=== `useEffect` [cols="2,3"] |=== |Signature |Behavior |`useEffect(EffectCallback effect)` |Runs after every render |`useEffect(EffectCallback effect, JSObject[] deps)` |Runs when `deps` change; pass `deps()` for mount-only |=== `EffectCallback` is a `@JSFunctor` that returns a `VoidCallback` (cleanup) or `null`: [source,java]
Hooks.useEffect) → { // setup return () → { /* cleanup */ }; }, Hooks.deps(;
=== `useRef` [cols="2,2"] |=== |Signature |Returns |`useRef(JSObject initial)` |`RefHandle` |`useRefInt(int initial)` |`RefHandle` |`useRefString(String initial)` |`RefHandle` |=== ==== `RefHandle` API * `getCurrentString()` / `setCurrentString(String)` -- String ref value * `getCurrentInt()` / `setCurrentInt(int)` -- int ref value * `getCurrentBool()` / `setCurrentBool(boolean)` -- boolean ref value * `getCurrentDouble()` / `setCurrentDouble(double)` -- double ref value === `useMemo` [source,java]
JSObject result = Hooks.useMemo) → expensiveComputation(), Hooks.deps(dep1, dep2;
The `MemoFactory` interface: [source,java]
@JSFunctor public interface MemoFactory extends JSObject { JSObject create(); }
=== `useCallback` [source,java]
JSObject memoizedCallback = Hooks.useCallback(callbackObj, Hooks.deps(dep1));
Memoizes a JS function reference so it doesn't change between renders unless deps change. === `useReducer` [source,java]
JSObject[] result = Hooks.useReducer(reducerFn, initialState);
The reducer is a JS function `(state, action) => newState`. === `useContext` Use the typed methods on `ReactContext` directly: [source,java]
String theme = themeCtx.useString(); int count = countCtx.useInt(); boolean auth = authCtx.useBool();
See <<context-api>> for details. === `deps()` Helper [source,java]
Hooks.deps() // empty array [] — mount only Hooks.deps(dep1, dep2, dep3) // specific dependencies
== Events Reference All event handler interfaces are in `ca.weblite.teavmreact.events` and are annotated with `@JSFunctor`. === Handler Interfaces [cols="2,2,2"] |=== |Interface |Method |Used For |`EventHandler` |`handleEvent(SyntheticEvent event)` |`onClick`, `onMouseDown`, `onMouseUp`, `onMouseEnter`, `onMouseLeave` |`ChangeEventHandler` |`handleEvent(ChangeEvent event)` |`onChange` |`KeyboardEventHandler` |`handleEvent(KeyboardEvent event)` |`onKeyDown`, `onKeyUp` |`FocusEventHandler` |`handleEvent(FocusEvent event)` |`onFocus`, `onBlur` |`SubmitEventHandler` |`handleEvent(SubmitEvent event)` |`onSubmit` |=== === `ChangeEvent` `ChangeEvent` extends `SyntheticEvent`. Access the input's value via the inherited `getTarget()` method, which returns an `EventTarget`: [source,java]
public interface ChangeEvent extends SyntheticEvent { // Inherits getTarget() → EventTarget with: // getValue(), getChecked(), getType(), getId(), getTagName(), getClassName() }
Usage: [source,java]
input("text") .onChange(e → name.setString(e.getTarget().getValue())) .build()
input("checkbox") .onChange(e → active.setBool(e.getTarget().getChecked())) .build()
=== `KeyboardEvent` [source,java]
public interface KeyboardEvent extends SyntheticEvent { @JSProperty String getKey(); // "Enter", "Escape", "a", etc. @JSProperty String getCode(); // "KeyA", "Enter", etc. @JSProperty boolean getAltKey(); @JSProperty boolean getCtrlKey(); @JSProperty boolean getMetaKey(); @JSProperty boolean getShiftKey(); @JSProperty boolean getRepeat(); // getTarget() inherited from SyntheticEvent }
=== `MouseEvent` [source,java]
public interface MouseEvent extends SyntheticEvent { @JSProperty double getClientX(); @JSProperty double getClientY(); @JSProperty double getPageX(); @JSProperty double getPageY(); @JSProperty int getButton(); // 0=left, 1=middle, 2=right @JSProperty boolean getAltKey(); @JSProperty boolean getCtrlKey(); @JSProperty boolean getMetaKey(); @JSProperty boolean getShiftKey(); // getTarget() inherited from SyntheticEvent }
[[context-api]] == Context API React Context allows passing data through the component tree without prop drilling. === Java API [source,java]
static final ReactContext THEME_CTX = ReactContext.create("light");
THEME_CTX.provide("dark", childElement1, childElement2 );
static ReactElement themeConsumer(JSObject props) { String theme = THEME_CTX.useString(); return Html.p("Current theme: " + theme); }
==== `ReactContext` API * `ReactContext.create()` -- create with no default value * `ReactContext.create(String defaultValue)` -- create with String default * `ReactContext.create(int defaultValue)` -- create with int default * `ReactContext.create(boolean defaultValue)` -- create with boolean default * `.useString()` / `.useInt()` / `.useBool()` -- consume the context value (typed) * `.provide(String value, ReactElement... children)` -- wrap children with String provider * `.provide(int value, ReactElement... children)` -- wrap children with int provider * `.provide(boolean value, ReactElement... children)` -- wrap children with boolean provider === Kotlin Typed Context See <<typed-context>> in the Kotlin DSL section. The Kotlin module provides type-safe wrappers: [source,kotlin]
val ThemeCtx = createStringContext("light")
ThemeCtx.provide("dark") { +App }
val theme: String = useContext(ThemeCtx)
== React.memo `React.memo()` wraps a component for shallow-prop comparison, skipping re-renders when props haven't changed: [source,java]
JSObject myComponent = React.wrapComponent(App::expensiveRender, "Expensive"); JSObject memoized = React.memo(myComponent);
Html.component(memoized);
This is equivalent to `React.memo(Component)` in JavaScript. == Building and Running === Maven Commands [source,bash]
mvn clean install <1> mvn clean install -N <2> mvn install -pl teavm-react-core <3> mvn install -pl teavm-react-core,teavm-react-kotlin <4> mvn process-classes -pl teavm-react-demo <5> mvn test -pl teavm-react-core,teavm-react-kotlin <6>
<1> Build the entire project <2> Install parent POM only (needed before building individual modules) <3> Build core library <4> Build core + kotlin <5> Run TeaVM compilation for the demo <6> Run unit tests Integration tests (TeaVM compilation + output verification): [source,bash]
mvn install -N && \ mvn install -pl teavm-react-core,teavm-react-kotlin -DskipTests && \ mvn process-classes test -pl teavm-react-demo
=== `run.sh` Script The project includes a convenience script that builds and serves the demo: [source,bash]
The script: 1. Installs the parent POM 2. Installs `teavm-react-core` 3. Runs TeaVM compilation on the demo module 4. Verifies `classes.js` was produced 5. Serves `teavm-react-demo/target/webapp/` via Python's HTTP server === Live-Reload Development Server For iterative development, the project includes a live-reload dev server that watches source files, recompiles automatically, and refreshes the browser on every save. [source,bash]
Edit any `.java`, `.kt`, `.html`, or `.css` file in your preferred editor. The browser updates automatically. ==== How it works The dev server (`DevServer.java`) is a single-file Java 21 program using only JDK built-in APIs: 1. **HTTP server** (`com.sun.net.httpserver.HttpServer`) serves `target/webapp/` 2. **File watcher** (`java.nio.file.WatchService`) recursively monitors all `src/` directories 3. **SSE endpoint** (`/__dev/events`) pushes reload notifications to the browser 4. **Script injection** — a live-reload `<script>` is injected into HTML responses automatically; no changes to `index.html` are required When a source file changes, the server debounces for 400ms (to batch rapid saves), recompiles via Maven, and sends a `reload` event to all connected browsers. During compilation, the browser shows a "Recompiling..." overlay. ==== Build optimizations The dev server activates the `dev` Maven profile, which tunes TeaVM for fast iteration: [cols="1,2",options="header"] |=== | Optimization | Effect | Incremental TeaVM (`<incremental>true</incremental>`) | Caches analysis results in `target/teavm-cache/`; only re-analyzes classes whose bytecode changed. This is the single biggest speedup. | No source maps (`<sourceMapsGenerated>false</sourceMapsGenerated>`) | Skips `.js.map` generation — saves I/O. | No debug info (`<debugInformationGenerated>false</debugInformationGenerated>`) | Skips debug metadata generation. | Smart module detection | If only `teavm-react-demo/src` changed, core and kotlin modules are not rebuilt. | Static fast-path | HTML/CSS-only changes in `src/main/webapp/` are copied directly to `target/webapp/` — Maven is not invoked at all. | Maven Daemon (mvnd) | Automatically detected and used if available. Eliminates JVM cold-start overhead (~2-3s per build). | Parallel module builds (`-T 1C`) | Core and kotlin modules build concurrently when both need rebuilding. |=== ==== Installing Maven Daemon (optional) For an additional speed boost, install https://github.com/apache/maven-mvnd[mvnd]. The dev server detects it automatically. * macOS: `brew install mvndaemon/homebrew-mvnd/mvnd` * Linux/Windows: download from the https://github.com/apache/maven-mvnd/releases[releases page] ==== `dev` Maven profile The `dev` profile is defined in `teavm-react-demo/pom.xml` and overrides the default TeaVM plugin configuration for fast iteration. You can also activate it manually for any Maven command: [source,bash]
mvn process-classes -pl teavm-react-demo -Pdev
For production builds, omit `-Pdev` to get full source maps and debug information. === TeaVM Maven Plugin Configuration The TeaVM plugin compiles Java bytecode to JavaScript. Configuration from the demo module: [source,xml]
<plugin> <groupId>org.teavm</groupId> <artifactId>teavm-maven-plugin</artifactId> <version>${teavm.version}</version> <executions> <execution> <goals><goal>compile</goal></goals> <phase>process-classes</phase> <configuration> <mainClass>com.example.App</mainClass> <targetType>JAVASCRIPT</targetType> <minifying>false</minifying> <debugInformationGenerated>true</debugInformationGenerated> <sourceMapsGenerated>true</sourceMapsGenerated> <stopOnErrors>true</stopOnErrors> <targetDirectory> ${project.build.directory}/webapp/js </targetDirectory> </configuration> </execution> </executions> </plugin>
Key settings: * `mainClass` -- your application's entry point (must have `public static void main(String[])`) * `targetType` -- always `JAVASCRIPT` for web apps * `targetDirectory` -- where `classes.js` is written * `minifying` -- set to `true` for production (smaller output, no debug names) * `debugInformationGenerated` / `sourceMapsGenerated` -- enable for development === HTML Shell Pattern Your `index.html` must: 1. Load React 18 and ReactDOM 18 via `<script>` tags *before* `classes.js` 2. Include a `<div id="root"></div>` mount point 3. Load the TeaVM-compiled `classes.js` 4. Call `main([])` to start the application [source,html]
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script> <div id="root"></div> <script src="js/classes.js"></script> <script>main([]);</script>
For production, use the `.production.min.js` variants of the React CDN URLs. == Maven Dependency Setup To use teavm-react in your own project, add these dependencies: === Core (Java only) [source,xml]
<dependencies> <!-- teavm-react core library -→ <dependency> <groupId>ca.weblite</groupId> <artifactId>teavm-react-core</artifactId> <version>0.1.0-SNAPSHOT</version> </dependency>
<!-- TeaVM standard library (required) -->
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-classlib</artifactId>
<version>0.13.1</version>
</dependency>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso</artifactId>
<version>0.13.1</version>
</dependency>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso-apis</artifactId>
<version>0.13.1</version>
</dependency>
</dependencies>
=== With Kotlin DSL [source,xml]
<dependency> <groupId>ca.weblite</groupId> <artifactId>teavm-react-kotlin</artifactId> <version>0.1.0-SNAPSHOT</version> </dependency>
<!-- Kotlin standard library -→ <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> <version>1.9.25</version> </dependency>
<!-- Coroutines (for effects, flows) -→ <dependency> <groupId>org.jetbrains.kotlinx</groupId> <artifactId>kotlinx-coroutines-core</artifactId> <version>1.8.1</version> </dependency>
Add the Kotlin Maven plugin for compilation: [source,xml]
<plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <version>1.9.25</version> <executions> <execution> <id>compile</id> <goals><goal>compile</goal></goals> <configuration> <sourceDirs> <sourceDir>${project.basedir}/src/main/kotlin</sourceDir> </sourceDirs> </configuration> </execution> </executions> </plugin>
=== TeaVM Plugin Add the TeaVM Maven plugin to your build (see <<teavm-maven-plugin-configuration>> for full configuration). === Java Version The project targets Java 11 source/target compatibility but requires JDK 21 for building: [source,xml]
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties>
== Testing === Running Tests Unit tests (core + kotlin modules): [source,bash]
mvn test -pl teavm-react-core,teavm-react-kotlin
Integration tests (TeaVM compilation + output verification): [source,bash]
mvn install -N mvn install -pl teavm-react-core,teavm-react-kotlin -DskipTests mvn process-classes test -pl teavm-react-demo
=== Test Patterns The project uses JUnit 5. Existing tests include: * *API surface tests* -- verify that key classes and methods exist and are accessible * *TeaVM compilation integration test* -- verifies that `classes.js` is produced and has reasonable size * *Kotlin unit tests* -- CSS parsing (`CssParsingTest`), string list encoding (`StringListEncodingTest`), API surface (`ApiSurfaceTest`) === CI Pipeline The GitHub Actions CI workflow (`.github/workflows/ci.yml`) runs on every push and PR to `main`: 1. Checks out code 2. Sets up JDK 21 (Temurin) 3. Compiles all modules 4. Runs unit tests (core + kotlin) 5. Runs integration tests (TeaVM compilation + verification) == Troubleshooting === "React is not defined" React must be loaded via `<script>` tags *before* `classes.js`. Ensure your HTML loads the React and ReactDOM CDN scripts first. === "classes.js not produced" * Verify the `mainClass` in the TeaVM plugin configuration matches your entry point * Ensure all dependencies are installed (`mvn install` on parent and core modules first) * Check for compilation errors in the Maven output === "No ES6 spread in @JSBody" TeaVM's `@JSBody` annotation does not support ES6 syntax including the spread operator (`...`). Use `.apply/.concat` instead: [source,javascript]
React.createElement.apply(null, [type, props].concat(children))
=== "Event handler not firing" Event handlers must be set using the dedicated `React.setOn*` methods, not `React.setProperty`. If you're building custom components, use `ElementBuilder` or `DomBuilder` which handle this correctly. See <<critical-implementation-details>>. === "Rules of Hooks violation" All hooks (`useState`, `useEffect`, `useRef`, etc.) must be called: * At the *top level* of the render function (not inside conditionals or loops) * In the *same order* every render * Only inside a component render function (inside a `RenderFunction` or `ReactView.render()`) This applies equally to Java and Kotlin. In `ReactView`, hooks declared as field initializers satisfy this requirement naturally. === "State not updating / stale closure" Use functional updates when the new state depends on the previous value: [source,java]
count.setInt(count.getInt() + 1);
count.updateInt(n → n + 1);
=== TeaVM Java API limitations TeaVM provides a subset of the Java standard library. Notable unsupported features include: * Reflection (beyond basic `Class` metadata) * `java.nio` channels * `java.net` networking * Some `java.util.concurrent` classes Check the https://teavm.org/docs/runtime/[TeaVM runtime documentation] for the full list of supported APIs. == Contributing === Adding a New HTML Element To add support for a new HTML element (e.g., `<dialog>`): 1. *`Html.java`* -- add `dialog(String text)` and `dialog(ReactElement... children)` methods 2. *`DomBuilder.java`* -- add a `public static final class Dialog extends DomBuilder` inner class 3. *`HtmlBuilder.kt`* -- add `fun dialog(block: HtmlBuilder.() -> Unit)` as both a nested method and a top-level function === Adding a New Event Handler To add support for a new event type: 1. Create the event interface in `ca.weblite.teavmreact.events` (with `@JSProperty` getters) 2. Create the handler interface with `@JSFunctor` 3. Add a `React.setOn*` method in `React.java` using `@JSBody` 4. Wire it into `ElementBuilder.java`, `DomBuilder.java`, and `HtmlBuilder.kt` === Code Style * Java source/target: 11 * No ES6 in `@JSBody` scripts * Event handlers always through dedicated setter methods * Kotlin follows standard conventions with `@HtmlDsl` marker annotation === Project Build Order When making changes across modules, build in order: [source,bash]
mvn install -N # parent POM mvn install -pl teavm-react-core # core first mvn install -pl teavm-react-kotlin # kotlin depends on core mvn process-classes -pl teavm-react-demo # demo depends on both