React4j

Input Step Enhancers

Edit this page

Use @Input(enhancer=...) to add presets or conversions to an input's generated builder step. The enhancer is an interface with one result type parameter and one abstract method matching the input's generated setter name and builder-facing value type. Its public default methods return the result parameter directly and delegate to that setter.

package react4j.examples.input_enhancers;

public interface ColorEnhancer<R> {
    R color(String value);

    default R red() {
        return color("red");
    }
}

Select it on a method input or constructor parameter:

package react4j.examples.input_enhancers;

import react4j.ReactNode;
import react4j.annotations.Input;
import react4j.annotations.Render;
import react4j.annotations.View;

@View(type = View.Type.STATEFUL)
abstract class Swatch {
    @Input(enhancer = ColorEnhancer.class)
    abstract String color();

    @Render
    ReactNode render() {
        return ReactNode.of(color());
    }
}

Helpers on the first step also have static shortcuts:

package react4j.examples.input_enhancers;

import react4j.ReactNode;

public final class Presets {
    private Presets() {}

    public static ReactNode redSwatch() {
        return SwatchBuilder.red();
    }
}

The result parameter describes what setting the input returns:

Setter behavior Helper result
More required inputs remain The next builder step
Several optional inputs share the step The same builder step
Setting this input finishes construction ReactNode

A terminal helper returns the finished node; it needs no additional build() call. Helpers preserve the existing ordering, required calls, defaults and canonical setters. An optional input may itself be terminal, so use the generated return type to determine whether construction has finished.

Enhancers may inherit their setter and default methods, including from compiled libraries. Helpers can declare their own method type parameters, varargs and checked exceptions; static shortcuts retain those contracts. Keep the interface accessible from the generated builder's package and follow the project's JSpecify configuration. Its setter must accept only values that the input can accept; a generated nullable setter may implement an enhancer setter that accepts non-null values. The abstract setter has no method type parameters or declared exceptions. The interface's result parameter has no Java bound beyond Object.

Each generated builder uses one implementation for all its steps. The processor rejects incompatible helper signatures, erased overloads and generic parent interfaces across those steps. Use distinct names such as compactLayout() and untitled() when two inputs would otherwise introduce different preset() methods. Compatible shared inherited defaults and ordinary overloads are allowed.

The interface has only its one abstract input setter; additional abstract capabilities are rejected. Private and static interface utilities remain implementation details. Tree-context inputs have no builder step and cannot select an enhancer.

Composing ready builders

A step that exposes build() also implements Buildable. Both Buildable and ReactNode extend Renderable, which composition APIs accept. Supply a ready builder directly to children, DOM helpers, context providers, roots or portals; the receiving API calls build() immediately. Each occurrence builds separately, and exceptions propagate. There is no implicit caching or freezing.

For example, supplying the required message leaves this builder ready, with an optional class name:

package react4j.examples.input_enhancers;

import org.jspecify.annotations.Nullable;
import react4j.ReactNode;
import react4j.annotations.Input;
import react4j.annotations.Render;
import react4j.annotations.View;
import react4j.dom.DOM;
import react4j.dom.proptypes.html.HtmlProps;

@View(type = View.Type.STATEFUL)
abstract class Notice {
    @Input
    abstract String message();

    @Input
    abstract @Nullable String className();

    @Render
    ReactNode render() {
        return DOM.p(new HtmlProps().className(className()), message());
    }
}

Declare an @Render method's return type as Renderable when it may return a ready builder:

package react4j.examples.input_enhancers;

import react4j.Renderable;
import react4j.annotations.Render;
import react4j.annotations.View;

@View(type = View.Type.STATEFUL)
abstract class NoticeHost {
    @Render
    Renderable render() {
        return NoticeBuilder.message("Saved");
    }
}

Existing ReactNode return types and explicit build() calls remain valid. A Java variable, field or helper declared as ReactNode still requires a materialized node; use Renderable when it should also hold a ready builder. Steps with required calls remaining cannot be supplied as renderables. Context consumer callbacks and generated render bridges build their returned value on each invocation.

Children inputs

An input whose effective name is children automatically selects react4j.ChildrenEnhancer. It supplies children(Stream<? extends Renderable>), which consumes the stream once in encounter order and delegates to the array setter. No enhancer annotation is needed:

package react4j.examples.input_enhancers;

import org.jspecify.annotations.Nullable;
import react4j.ReactNode;
import react4j.annotations.Input;
import react4j.annotations.Render;
import react4j.annotations.View;

@View(type = View.Type.STATEFUL)
abstract class ChildrenPanel {
    @Input
    abstract @Nullable String title();

    @Input
    abstract ReactNode @Nullable [] children();

    @Render
    ReactNode render() {
        final var title = title();
        final var children = children();
        return ReactNode.of(
                null == title ? null : ReactNode.of(title), null == children ? null : ReactNode.of(children));
    }
}

Both child and children always finish construction and return ReactNode, including when other optional inputs share their step. Set optional properties first, then supply the children. Optional children may still be omitted through build(). First-step stream helpers also have static shortcuts:

package react4j.examples.input_enhancers;

import java.util.stream.Stream;
import react4j.ReactNode;

public final class ChildrenPresets {
    private ChildrenPresets() {}

    public static ReactNode titledPanel() {
        return ChildrenPanelBuilder.title("Items")
                .children(Stream.of(NoticeBuilder.message("Saved"), NoticeBuilder.message("Synchronized")));
    }

    public static ReactNode untitledPanel() {
        return ChildrenPanelBuilder.children(Stream.of(NoticeBuilder.message("Saved")));
    }

    public static ReactNode emptyMenu() {
        return MenuPanelBuilder.empty();
    }
}

An explicit enhancer replaces the automatic selection. Extend ChildrenEnhancer to keep its stream helper while adding presets:

package react4j.examples.input_enhancers;

import react4j.ChildrenEnhancer;

public interface MenuChildren<R> extends ChildrenEnhancer<R> {
    default R empty() {
        return children();
    }
}
package react4j.examples.input_enhancers;

import react4j.ReactNode;
import react4j.annotations.Input;
import react4j.annotations.Render;
import react4j.annotations.View;

@View(type = View.Type.STATEFUL)
abstract class MenuPanel {
    @Input(enhancer = MenuChildren.class)
    abstract ReactNode[] children();

    @Render
    ReactNode render() {
        return ReactNode.of(children());
    }
}

A standalone custom enhancer supplies only its own helpers, so it can also provide its own stream overload. Omitting enhancer or specifying void.class selects the standard default for children and no enhancer for other inputs. Tree-context inputs do not receive the default.

Generated setters for ReactNode and ReactNode[] inputs now accept Renderable and Renderable[], with the same nullness. The view's input declarations can keep their node types: builders are materialized before storage. Update custom node-input enhancer setters to those widened types. Overrides of the standard children stream helper must use Stream<? extends Renderable> as well. Ordinary data inputs and untyped property maps are not traversed for builders.

When migrating existing chains, move setters after child or children before that call and remove the trailing build():

// Before
BreadcrumbBuilder.label("Overview").child(menu).leaf(false).build();
// After
BreadcrumbBuilder.label("Overview").leaf(false).child(menu);