Docs

Documentation versions (currently viewingVaadin 24)

Using React Components in Flow

Using React components in Flow.

React is a popular JS-based frontend UI library with many components and is widely adopted. This documentation page explains how to integrate existing React components with Flow by using an intermediate adapter Web Component, and then using it in Java applications.

This integration involves two parts: creating a Java class for the server-side adapter component; and a client-side adapter counterpart with TypeScript and React. For examples here, the React Colorful color picker component is used.

Server-Side Adapter Java Component

For the server-side part of the integration, create a Java class that extends from ReactAdapterComponent like so:

@NpmPackage(value = "react-colorful", version = "5.6.1") // (1)
@JsModule("./demo/flow/integration/react/rgba-color-picker.tsx") // (2)
@Tag("rgba-color-picker") // (3)
public class RgbaColorPicker extends ReactAdapterComponent {


}
  1. Since this will integrate a third-party React component, the @NpmPackage annotation is added to be installed from npm.

  2. The @Tag annotation defines the DOM element name of the adapter Web Component (see below).

  3. The client-side part of the integration (i.e., the adapter Web Component) is yet to be defined and implemented in TypeScript. Therefore, you’ll need a separate file for it. Although this file will be created later, the @JsModule annotation is added here to import it.

Java & React States Synchronized

The Web Component adapter allows the state to be sent from Java to React and back by using named properties and events in the intermediate Web Component. On the server side, the ReactAdapterComponent class provides the following APIs for synchronizing the state with the adapter Web Component:

  • setState(String, T) method is for sending the state from the server;

  • getState(String, Class<T>) method is used for retrieving the current value from the client; and

  • addStateChangeListener(String, Class<T>, Consumer<T>) adds a server-side listener for state changes in the client.

You can use these methods to implement Java component property getters and setters, and event APIs. The example here uses the synchronized state for the getColor() and setColor(RgbaColor) accessor methods:

public class RgbaColorPicker extends ReactAdapterComponent {

    public RgbaColor getColor() {
        return getState("color", RgbaColor.class);
    }

    public void setColor(RgbaColor color) {
        setState("color", color);
    }

}
Note
Setting Initial Value
Call setState from the Java constructor to ensure the state is initialized from Java. Doing so enables @PreserveOnRefresh to work correctly.

Next, invoke setColor(RgbaColor) from the constructor to define the initial state like so:

public class RgbaColorPicker extends ReactAdapterComponent {

    public RgbaColorPicker() {
        setColor(new RgbaColor(255, 0, 0, 0.5));
    }

}

Also, you’ll need to add the addColorChangeListener(Consumer<T>) state change callback:

public class RgbaColorPicker extends ReactAdapterComponent {

    public void addColorChangeListener(
            SerializableConsumer<RgbaColor> listener) {
        addStateChangeListener("color", RgbaColor.class, listener);
    }

}

Using Non-Primitive Types

On the client, the React state can be any JS value. To support that, the methods above accept Java boxed primitive types, as well as Beans and collections that are representable with JSON.

In the example here, the component has the color property, which is a JS object with the following format:

type RgbaColor = {
  r: number;
  g: number;
  b: number;
  a: number;
};

For representing such object types in Java, an easy option is to define a Bean using a record. The following example shows the RgbaColor bean, which was used in the RgbaColorPicker example above:

public record RgbaColor(int r, int g, int b, double a) {

    @Override
    public String toString() {
        return "{ r: %s, g: %s, b: %s, a: %s }".formatted(r, g, b, a);
    }

}

Client-Side Adapter Web Component

Flow provides the ReactAdapterElement TypeScript class for implementing Web Components that render with React.

The following example uses ReactAdapterElement to create the <rgba-color-picker> Web Component, which renders the <RgbaColorPicker> third-party React component in the client:

class RgbaColorPickerElement extends ReactAdapterElement {
  protected override render(hooks: RenderHooks): ReactElement | null { // (1)
    const [color, setColor] = hooks.useState<RgbaColor>('color'); // (2)

    return <RgbaColorPicker color={color} onChange={setColor} />; // (3)
  }
}

customElements.define('rgba-color-picker', RgbaColorPickerElement); // (4)
  1. The main part of the Web Component is the render(RenderHooks) callback, which defines the rendering using React JSX tags. You could also define state and events using the APIs provided with the hooks argument.

  2. This calls hooks.useState<RgbaColor>("color") to define the color property of the adapter Web Component that is to be used for synchronizing the state with the server.

  3. The JSX rendering uses the adapter state (i.e., color and setColor) with the <RgbaColorPicker> React component, following the third-party component’s API.

  4. As usual with Web Components, the JS class is registered for its DOM element name using CustomElements.define() standard web API.

RenderHooks APIs

The RenderHooks render callback argument provides the following two methods:

  • useState<T>(string) method defines and uses a named state JS property of the adapter Web Component; and

  • useCustomEvent<T | undefined>(string, CustomEventInit<T>) method returns a simple-to-use callback that dispatches a Custom Event with the given name on the adapter Web Component.

interface PersonData {
    name: string,
    age?: number
}

class PersonDataElement extends ReactAdapterElement {
    protected override render(hooks: RenderHooks): ReactElement | null {
        const fireUpdateEvent = hooks.useCustomEvent<PersonData>("update",
            // optional event detail:
            { detail: { name: "John", age: 42 } }
        );
        return (
            <>
                <VerticalLayout>
                    <Button onClick={() => fireUpdateEvent({ name: "Mike" })}>
                        Update
                    </Button>
                </VerticalLayout>
            </>
        );
    }
}

When the user action is not related to a state change, you could use this hook and subscribe to the event in Java:

public void addPersonDataUpdateListener() {
    getElement().addEventListener("update", event -> {
        JsonObject person = event.getEventData();
        // person update code goes here...
    }).addEventData("event.detail");
}

Adding Flow Components to React Component

It’s possible to put Flow Component elements inside a React component implemented with ReactAdapterElement. You could generate a RouterLayout in React that has a navigation menu and the Flow route content on the right side. See RouterLayout Interface for more information.

Create a ReactAdapterComponent that implements the RouterLayout interface so that the showRouterLayoutContent is appended into the content element from getContentElement(String).

RouterLinks are added to a Div that’s bound to the menu content element on the client.

@JsModule("./ReactRouterLayout.tsx")
@Tag("react-router-layout")
public class ReactRouterLayout extends ReactAdapterComponent implements RouterLayout {

    public ReactRouterLayout() {
        Div links = new Div(
                new RouterLink("Main view", MainView.class),
                new Div(),
                new RouterLink("Information", InfoView.class)
        );
        links.getStyle().setMargin("10px");
        getContentElement("menu").appendChild(links.getElement());
    }

    @Override
    public void showRouterLayoutContent(HasElement content) {
        if (content != null) {
            // Bind the flow element to the 'flowContent' element on the client
            getContentElement("flowContent")
                    .appendChild(Objects.requireNonNull(content.getElement()));
        }
    }
}

On the client, there are two places for Flow components to bind: one for menu; and one for flowContent, using the userContent(string).

class ReactRouterLayoutElement extends ReactAdapterElement {
    protected render(hooks: RenderHooks): React.ReactElement | null {
        // create content element for name attribute 'menu'
        const menu = hooks.useContent('menu');
        // create content element for name attribute 'flowContent'
        const content = hooks.useContent('flowContent');

        return <div style={{display: 'flex', flex: 1, flexDirection: 'row', height: '100%'}}>
            <div style={{flex: 1}}>{menu}</div>
            <div style={{flex: 5}}>{content}</div>
        </div>;
    }
}

customElements.define('react-router-layout', ReactRouterLayoutElement);

Wrapping a React Input Component into a Flow Field Component

When integrating React components into Vaadin applications, one common requirement is to enable these components to participate in Vaadin’s data binding and form handling. This can be accomplished by wrapping the React component in a Vaadin component that extends AbstractSinglePropertyField. This allows the React component to be used like any other field in Vaadin, making it compatible with the Binder API.

This integration process involves two main parts: creating a Java class for the server-side adapter component; and developing a client-side adapter using TypeScript and React.

This process is demonstrated in the next sections using a simple React text input component as an example.

Create Client-Side React Component

First, define your React component. For this example, assume a simple text input component.

// File: frontend/react-text-input.tsx

import React, { useState } from 'react';
import { ReactAdapterElement, type RenderHooks } from 'Frontend/generated/flow/ReactAdapter';

class ReactTextInputElement extends ReactAdapterElement {
    constructor() {
        super();
    }

    protected override render(hooks: RenderHooks): React.ReactElement | null {
        const [value, setValue] = hooks.useState<string>('value');

        const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
            setValue(event.target.value);
            this.dispatchEvent(new CustomEvent('value-changed', { detail: { value: event.target.value } }));
        };

        return (
            <div>
                <input type="text" value={value} onChange={handleChange} />
                <span>{value?.length} characters</span>
            </div>
        );
    }
}

customElements.define('react-text-input', ReactTextInputElement);

Server-Side Vaadin Component Wrapping the React Component

Next, create a Vaadin component that wraps the React component and extends AbstractSinglePropertyField.

// Package: com.example.application.ui

import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.AbstractSinglePropertyField;

@Tag("react-text-input")
@JsModule("./react-text-input.tsx")
public class ReactTextField extends AbstractSinglePropertyField<ReactTextField, String> {
    public ReactTextField() {
        super("value", "", false);
    }
}

Using React Component in a Vaadin Form

Now, you can use ReactTextField like any other Vaadin component within a form:

Binder<Person> binder = new Binder<>(Person.class);
ReactTextField reactTextField = new ReactTextField();

binder.forField(reactTextField).bind(Person::getName, Person::setName);

add(reactTextField);