Docs

Documentation versions (currently viewingVaadin 24)
Documentation translations (currently viewingEnglish)

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:

Source code
RgbaColorPicker.java
@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 {


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

  2. 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.

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

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:

Source code
RgbaColorPicker.java
public class RgbaColorPicker extends ReactAdapterComponent {

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

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

}
RgbaColorPicker.java
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:

Source code
RgbaColorPicker.java
public class RgbaColorPicker extends ReactAdapterComponent {

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

}
RgbaColorPicker.java

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

Source code
RgbaColorPicker.java
public class RgbaColorPicker extends ReactAdapterComponent {

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

}
RgbaColorPicker.java

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:

Source code
TypeScript
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:

Source code
RgbaColor.java
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);
    }

}
RgbaColor.java

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:

Source code
rgba-color-picker.tsx
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
rgba-color-picker.tsx
  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.

Source code
Firing a CustomEvent
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:

Source code
Listening for a CustomEvent
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.

Source code
Server Router Layout
@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).

Source code
Client Router Layout
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.

Source code
jsx
// 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.

Source code
Java
// 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:

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

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

add(reactTextField);