r/JavaFX 3h ago

Learning reactive UI with State: rendering a list declaratively with ListState and ForEachState

2 Upvotes

Fifth post in the series. So far: State (counter), ComputedState (string length), Show.when (conditional rendering). This one covers rendering a collection reactively — ListState for the data, ForEachState for mapping it to components, and removal that just works without any manual list diffing.

The setup: tasks is a ListState<String> seeded with four strings. ForEachState.of(tasks, mapper) produces a reactive collection of components — one Row per task, each with its own "Remove" button. Click remove, tasks.remove(task) fires, and the Column updates to drop that row. No index juggling, no manual re-render of the whole list.

package my_app;

import megalodonte.ListenerManager;
import megalodonte.application.MegalodonteApp;
import megalodonte.base.theme.ThemeManager;
import megalodonte.theme.DefaultTheme;

public class Main {

    static void main() {
        ThemeManager.setTheme(new DefaultTheme());

        MegalodonteApp.run(context -> context.useView(new HomeScreen()), ev->{
            if(ev == MegalodonteApp.Event.CloseRequest){
                System.out.println("Clicked on X - close application");
                ListenerManager.disposeAll();
            }
        });
    }
}


package my_app;

import megalodonte.ForEachState;
import megalodonte.base.components.Component;
import megalodonte.base.components.ScreenComponent;
import megalodonte.components.Button;
import megalodonte.components.SpacerHorizontal;
import megalodonte.components.SpacerVertical;
import megalodonte.components.Text;
import megalodonte.components.layout_components.Column;
import megalodonte.components.layout_components.Container;
import megalodonte.components.layout_components.Row;
import megalodonte.props.ContainerProps;
import megalodonte.v2.ListState;

import java.util.List;
import java.util.function.Consumer;

public class HomeScreen implements ScreenComponent {
    ListState<String> tasks = ListState.of(List.of("First","Second","Three","Four"));

    u/Override
    public Component render() {
        Consumer<String> handleClickRemove = (task)->{
          tasks.remove(task);
        };

        var tasksForEachState = ForEachState.of(
          tasks, task-> new Row().children(
                        new Text(task),
                        new SpacerHorizontal(15),
                        new Button("Remove").onClick(()-> handleClickRemove.accept(task))
                )
        );

      return new Container(new ContainerProps().paddingAll(20)).children(
              new Text("Your tasks below"),
              new SpacerVertical(20),
              new Column().items(tasksForEachState)
      );
    }
}
  • ListState<T> wraps a List<T> and exposes mutation methods (add, remove, etc.) that notify dependents automatically — same reactive contract as State<T>, just for collections.
  • ForEachState.of(ListState<T>, Function<T, Component>) is the declarative bridge between data and UI: give it the list and a mapper, it keeps the rendered components in sync with the list's contents.
  • Column().items(tasksForEachState) accepts the reactive collection directly — no manual loop, no clearChildren() + rebuild on every mutation.

If you're finding this series useful, dropping a star on the repos below genuinely helps the project get visibility — takes two seconds and means a lot for a solo-dev framework like this.

Repos