State Management

JWeb provides reactive state management inspired by React hooks. State changes automatically trigger UI updates via WebSocket.

Overview

Use useState() to create reactive state variables. When state changes, the component re-renders automatically.

import static com.osmig.Jweb.framework.state.StateHooks.*;

// Create state with initial value
State<Integer> count = useState(0);

// Read: count.get()
// Write: count.set(newValue)
// Update: count.update(current -> newValue)

Creating State

Use useState to create reactive state in components.

import static com.osmig.Jweb.framework.state.StateHooks.*;

public class Counter implements Template {
    private final State<Integer> count = useState(0);

    public Element render() {
        return div(
            h1("Count: " + count.get()),
            button(attrs().onClick(e -> count.set(count.get() + 1)),
                text("Increment")),
            button(attrs().onClick(e -> count.set(count.get() - 1)),
                text("Decrement"))
        );
    }
}

Reading State

Use get() to read the current value.

State<String> name = useState("John");
State<Integer> age = useState(25);
State<Boolean> active = useState(true);

// Read values
String currentName = name.get();
int currentAge = age.get();
boolean isActive = active.get();

// Use in templates
p("Name: " + name.get())
p("Age: " + age.get())
when(active.get(), () -> span("Active"))

Setting State

Use set() to replace the value, update() to transform it.

State<String> name = useState("John");
State<Integer> count = useState(0);

// Direct set
name.set("Jane");
count.set(10);

// Update based on current value
count.update(c -> c + 1);
count.update(c -> c * 2);

// Toggle boolean
State<Boolean> visible = useState(false);
visible.update(v -> !v);

Batch Updates

Multiple updates in one event handler are batched.

State<Integer> x = useState(0);
State<Integer> y = useState(0);

button(attrs().onClick(e -> {
    x.set(100);  // Batched
    y.set(200);  // Batched
    // Single re-render after both updates
}), text("Move"))

Conditional Updates

State<Integer> count = useState(0);

// Only update if condition met
button(attrs().onClick(e -> {
    if (count.get() < 10) {
        count.update(c -> c + 1);
    }
}), text("Increment (max 10)"))

// Reset to initial
button(attrs().onClick(e -> count.set(0)), text("Reset"))

State with Objects

Store complex objects in state.

// User record or class
record User(String name, String email, boolean admin) {
    User withName(String name) {
        return new User(name, email, admin);
    }
    User withEmail(String email) {
        return new User(name, email, admin);
    }
}

State<User> user = useState(new User("John", "john@test.com", false));

// Update object
user.update(u -> u.withName("Jane"));
user.update(u -> u.withEmail("jane@test.com"));

// Display
div(
    p("Name: " + user.get().name()),
    p("Email: " + user.get().email()),
    when(user.get().admin(), () -> span("Admin"))
)

Nested Object Updates

record Address(String street, String city) {}
record Person(String name, Address address) {}

State<Person> person = useState(
    new Person("John", new Address("123 Main", "NYC"))
);

// Update nested property - create new objects
person.update(p -> new Person(
    p.name(),
    new Address("456 Oak", p.address().city())
));

State with Lists

Manage collections with reactive state.

State<List<String>> items = useState(new ArrayList<>());

// Add item
items.update(list -> {
    list.add("New item");
    return list;
});

// Remove item
items.update(list -> {
    list.remove(index);
    return list;
});

// Filter items
items.update(list -> list.stream()
    .filter(item -> !item.isEmpty())
    .collect(Collectors.toList()));

Todo List Example

record Todo(String text, boolean done) {}

State<List<Todo>> todos = useState(new ArrayList<>());

// Add todo
void addTodo(String text) {
    todos.update(list -> {
        list.add(new Todo(text, false));
        return list;
    });
}

// Toggle todo
void toggleTodo(int index) {
    todos.update(list -> {
        Todo old = list.get(index);
        list.set(index, new Todo(old.text(), !old.done()));
        return list;
    });
}

// Render
ul(each(todos.get(), (todo, i) ->
    li(
        input(attrs().type("checkbox")
            .checked(todo.done())
            .onChange(e -> toggleTodo(i))),
        span(todo.text())
    )
))

Derived State

Compute values from other state.

State<List<Todo>> todos = useState(new ArrayList<>());

// Derived values (recomputed on render)
int total = todos.get().size();
int completed = (int) todos.get().stream()
    .filter(Todo::done).count();
int remaining = total - completed;

div(
    p("Total: " + total),
    p("Completed: " + completed),
    p("Remaining: " + remaining)
)

Shared State

Share state between components via constructor.

// Parent owns the state
public class App implements Template {
    private final State<User> user = useState(null);

    public Element render() {
        return div(
            new Header(user),     // Pass state
            new Content(user),    // Same state
            new Footer(user)      // Same state
        );
    }
}

// Child receives and uses state
public class Header implements Template {
    private final State<User> user;

    public Header(State<User> user) {
        this.user = user;
    }

    public Element render() {
        return header(
            when(user.get() != null,
                () -> span("Welcome, " + user.get().name()))
        );
    }
}
State changes trigger automatic re-renders via WebSocket.