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 {
}
-
Since this will integrate a third-party React component, the
@NpmPackage
annotation is added to be installed from npm. -
The
@Tag
annotation defines the DOM element name of the adapter Web Component (see below). -
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)
-
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 thehooks
argument. -
This calls
hooks.useState<RgbaColor>("color")
to define thecolor
property of the adapter Web Component that is to be used for synchronizing the state with the server. -
The JSX rendering uses the adapter state (i.e.,
color
andsetColor
) with the<RgbaColorPicker>
React component, following the third-party component’s API. -
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);