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:
Syntax Highlighter
Image Crop
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!
Join the conversation!