VAADIN

Introducing our first Vaadin add-ons integrating React Components

15 August, 2024
Introducing our first Vaadin add-ons integrating React Components

Vaadin 24.4 brings exciting new capabilities for integrating React into Vaadin applications in various forms. This offers developers a range of integration options, providing unprecedented flexibility in building modern web applications. You can delve into the various possibilities and the reasons behind this improvement in Vaadin’s blog post.

As you already know, we love creating Vaadin add-ons, so we were thrilled to explore the possibility of creating add-ons that integrate React components. This, we believe, unlocks a whole new world of possibilities for Vaadin applications.

The First Steps

After the announcement, we immersed ourselves in the documentation to understand how to integrate React components with Vaadin Flow.

Then we selected a few React components that seemed interesting and manageable for a first attempt, and thus our adventure began.

Our first pick was a syntax highlighter component, and the second was a component that allows to crop images:

  • Syntax Highlighter: This component allows to display formatted code snippets in various programming languages, enhancing readability and presentation. It’s easy to implement and supports multiple languages and themes.
  • Image Crop: This component enables users to crop images offering numerous properties to adjust the cropping process to your particular needs.

The Process

The Vaadin documentation is pretty complete, so let’s summarize the key points for achieving this integration. There are two main parts to consider:

1 – Create a Client-Side Adapter Web Component:

  • Create a wrapper of the React component as a custom web component by extending ReactAdapterElement class.
  • Implement the render function to access the component’s state and events using hooks arguments.
  • As it is a web component, to exported just use the standard customElements.define().

For example, to integrate react-image-crop we created a image-crop.tsx file:

import { ReactAdapterElement, RenderHooks } from 'Frontend/generated/flow/ReactAdapter';
import { JSXElementConstructor, ReactElement, useRef } from "react";
import React from 'react';
import { type Crop, ReactCrop, PixelCrop, makeAspectCrop, centerCrop } from "react-image-crop";

class ImageCropElement extends ReactAdapterElement {

	protected render(hooks: RenderHooks): ReactElement<any, string | JSXElementConstructor<any>> | null {

		const [crop, setCrop] = hooks.useState<Crop>("crop");
		const [imgSrc] = hooks.useState<string>("imgSrc");
		const imgRef = useRef<HTMLImageElement>(null);
		const [imgAlt] = hooks.useState<string>("imgAlt");
		const [aspect] = hooks.useState<number>("aspect");
		const [circularCrop] = hooks.useState<boolean>("circularCrop", false);
		const [keepSelection] = hooks.useState<boolean>("keepSelection", false);
		const [disabled] = hooks.useState<boolean>("disabled", false);
		const [locked] = hooks.useState<boolean>("locked", false);
		const [minWidth] = hooks.useState<number>("minWidth");
		const [minHeight] = hooks.useState<number>("minHeight");
		const [maxWidth] = hooks.useState<number>("maxWidth");
		const [maxHeight] = hooks.useState<number>("maxHeight");
		const [ruleOfThirds] = hooks.useState<boolean>("ruleOfThirds", false);

		....

		return (
			<ReactCrop
				crop={crop}
				onChange={(c: Crop) => onChange(c)}
				onComplete={(c: PixelCrop) => onComplete(c)}
				circularCrop={circularCrop}
				aspect={aspect}
				keepSelection={keepSelection}
				disabled={disabled}
				locked={locked}
				minWidth={minWidth}
				minHeight={minHeight}
				maxWidth={maxWidth}
				maxHeight={maxHeight}
				ruleOfThirds={ruleOfThirds}
			>
				<img
					ref={imgRef}
					src={imgSrc}
					alt={imgAlt}
					onLoad={onImageLoad} />
			</ReactCrop>
		);
	}
	
}

customElements.define("image-crop", ImageCropElement);

And, for react-syntax-highlighter we created a react-syntax-highlighter.tsx file:

import {type ReactElement} from 'react';
import SyntaxHighlighter from 'react-syntax-highlighter';
import * as styles from 'react-syntax-highlighter/dist/esm/styles/hljs';
import {ReactAdapterElement, type RenderHooks} from "Frontend/generated/flow/ReactAdapter";
import React from 'react';

class SyntaxHighlighterElement extends ReactAdapterElement {

    protected override render(hooks: RenderHooks): ReactElement | null {
      const [language] = hooks.useState<string>("language"); 
      const [content] =  hooks.useState<string>("content");
      const [stylename] = hooks.useState<string>("stylename");
      const [showLineNumbers] = hooks.useState<boolean>("showLineNumbers");
      const [wrapLongLines] = hooks.useState<boolean>("wrapLongLines");
      
      return <SyntaxHighlighter 
      			language={language} 
      			style={styles[stylename]}
      			showLineNumbers={showLineNumbers}
	            wrapLongLines={wrapLongLines}
	            customStyle={{
	              width: "calc(100% - 16px)",
	              height: "calc(100% - 20px)"
	            }}
      			>
      			{content}
      		</SyntaxHighlighter>;
    }
}
  
customElements.define("syntax-highlighter",SyntaxHighlighterElement);

2 – Create a Server-Side Adapter Component:

  • Create a class extending ReactAdapterComponent that uses the recently created web component.
  • Use @NpmPackage to install the chosen React component, and then define the already known @Tag and @JsModule annotations to reference the web component.
  • To syncronize the states from/to the server-side to the adpater web component, use setState method for sending the state from the server and getState method for retrieving the current value from the client.

See an example on the implementation of the class ImageCrop.java:

@NpmPackage(value = "react-image-crop", version = "11.0.6")
@JsModule("./src/image-crop.tsx")
@Tag("image-crop")
@CssImport("react-image-crop/dist/ReactCrop.css")
public class ImageCrop extends ReactAdapterComponent {

  ....
  
  /**
   * Sets the source of the image to be cropped.
   *
   * @param imageSrc the image source
   */
  public void setImageSrc(String imageSrc) {
    setState("imgSrc", imageSrc);
  }

  /**
   * Gets the source of the image being cropped.
   *
   * @return the image source
   */
  public String getImageSrc() {
    return getState("imgSrc", String.class);
  }

  /**
   * Defines the crop dimensions.
   * 
   * @param crop the crop dimensions
   */
  public void setCrop(Crop crop) {
    setState("crop", crop);
  }

  /**
   * Gets the crop dimensions.
   */
  public Crop getCrop() {
    return getState("crop", Crop.class);
  }

  /**
   * Sets the aspect ratio of the crop.
   * For example, 1 for a square or 16/9 for landscape.
   * 
   * @param aspect the aspect ratio of the crop
   */
  public void setAspect(double aspect) {
    setState("aspect", aspect);
  }

  /**
   * Gets the aspect ratio of the crop.
   *
   * @return the aspect ratio
   */
  public double getAspect() {
    return getState("aspect", Double.class);
  }

 ....

 }

The Results

We took a significant step forward in integrating React components by creating two new add-ons:

We hope you find this add-ons useful and that they serve as a guide if you’re looking to integrate React components.

Conclusion

Before wrapping up, I would like to say that this was a fun adventure. Trying new things is always exciting, right? We believe this is a positive step for Vaadin, as it listens to and addresses the needs of developers.

What tips can we share about the integration?

  • Choose a React component that best fits your needs and has good documentation, plenty of examples, and regular maintenance.
  • Always read the Vaadin documentation before you start.
  • Learn to love React if you don’t already do!

Visit our open source section to learn more about all our Vaadin add-ons.

Thanks for reading and keep the code flowing!

Paola De Bartolo
By Paola De Bartolo

Systems Engineer. Java Developer. Vaadin enthusiast since the moment I heard "you can implement all UI with Java". Proud member of the #FlowingCodeTeam since 2017.

Join the conversation!
Profile Picture

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.