Theme:

Island-inspired Architecture - React in Umbraco

Tagged with:
Content
Frontend

Have you ever faced a situation where you have a standalone Umbraco site and want to add some advanced interactivity, but dread the thought of wrestling with complex vanilla JavaScript?

Perhaps you need a widget with intricate state requirements that has outgrown the capabilities of lightweight libraries like AlpineJS.

Or maybe you have a front-end team that are absolute wizards with React and can craft awesome user experiences, but going with a headless CMS and full-blown React app feels like overkill.

In this article, we’ll explore how the Island Architecture pattern can inspire us to introduce pockets of React within your existing Umbraco site.

We’ll cover how to get set up with React and discuss some key considerations to keep in mind if you’re exploring this approach.

The Islands Architecture Pattern

Islands Architecture is a design pattern that optimises web rendering by loading interactive components only where they’re needed - like islands in a sea of static content. This approach originated from the need to improve performance in modern web applications, enabling developers to embed dynamic, interactive elements within otherwise static pages.

You can read more about the concept here: Islands Architecture on Patterns.dev.

Adapting the Islands Architecture for Umbraco

You might be wondering, “Isn’t Umbraco traditionally a server-rendered CMS? How does the islands concept fit here?”.

While the purest form of the Islands Architecture is often associated with front-end meta frameworks and static site generators, the core principles can be adapted to enhance an Umbraco site.

It’s important to clarify that this isn’t about implementing a traditional islands setup as seen in frameworks like Astro. Instead, we’re borrowing the concept to introduce React components into specific parts of Umbraco pages.

You may have used similar techniques before to add small, interactive React widgets to a site, but without giving it a formal name.

Here we are formalising the process somewhat and directly acknowledging the Islands Architecture pattern as our inspiration.

Setting Up React Islands in Umbraco

In this example setup, we’ll use an Umbraco site alongside a front-end project containing our styles and React setup, leveraging TypeScript.

A complete working example can be found on GitHub here

Umbraco CMS

On the backend, we’re working with a plain v13 Umbraco install, along with the Clean Starter Kit to provide a foundation and some content to work with.

Front-End Dependencies

To start, we’ll install the front-end dependencies. For this example, we’re using Tailwind for styles and Vite for compiling and bundling our assets.

The dependencies are already configured in the GitHub repo, but if you are starting from scratch, you will need the following:


npm install react react-dom tailwindcss
npm install --save-dev typescript vite @vitejs/plugin-react vite-tsconfig-paths @types/react @types/react-dom @types/node shx

Basic Island Example 🏝️

Now we have our dependencies set up let's embark on creating a super simple React component as our first island.

For this we'll just have a React component that renders some text 'Hello Island!'.

Within our code, this can be added in frontend/components/HelloIsland.tsx

Or found in the repo here.


import React from 'react';

const HelloIsland = () => {
  return <div>Hello Island!</div>;
};

export default HelloIsland;

Now we have a basic React component, let's define a mechanic to allow us to place it in an Umbraco template.

For this, let's set a div with data attributes to help us define what type of component we want rendering with the following markup:


<div class="react-component" data-component="HelloIsland"></div>

In order to process this and any other React islands, we are going to create a top-level orchestrator to collect all the .react-component elements (defined by the class), then initialise them based on data-component attribute.

In frontend/scripts/main.tsx


import { createRoot } from "react-dom/client";
import HelloIsland from "@components/HelloIsland";
//import AnotherComponent from "AnotherComponent/AnotherComponent";

type ComponentMap = {
  // 'any' used for demo purposes
  // For better type safety, consider using a mapped type with specific prop definitions per component.
  [key: string]: (props: any) => JSX.Element;
};

const componentsMap: ComponentMap = {
  HelloIsland,
  //AnotherComponent,
  // Add more components as needed
};

document.addEventListener("DOMContentLoaded", () => {
  // Select all elements that should host a React component
  const elements = document.querySelectorAll(".react-component");

  elements.forEach((element) => {
    const componentName = element.getAttribute("data-component");
    if (!componentName) return;

    const Component = componentsMap[componentName];
    if (!Component) return;

    // Render the component into the element
    const root = createRoot(element);
    root.render(<Component />);
  });
});

We're using the main.tsx entry point for demo purposes here

Additional components can be registered in the orchestrator and added to the componentsMap as needed.

Bundling up the JavaScript for Umbraco

We now need to configure Vite and an NPM script to bundle up our JavaScript and copy it to our Umbraco project.

In frontend/vite.config.js


import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  plugins: [
    react(),
    tsconfigPaths({ projects: [resolve(__dirname, 'tsconfig.json')] })
  ],
  build: {
    outDir: resolve(__dirname, '../24Days.ReactIslands.Web/wwwroot/demo-assets'),
    rollupOptions: {
      input: resolve(__dirname, 'src/scripts/main.tsx'),
      output: {
        entryFileNames: 'main.min.js'
      },
    },
  },
});

Then a package.json NPM script to run Vite and also copy Tailwind styles and created JS bundle over to Umbraco:


"scripts": {
    "build:tailwind": "tailwindcss -i ./src/styles/tailwind.css -o ./build/tailwind-build.css --minify && shx cp ./build/tailwind-build.css ../24Days.ReactIslands.Web/wwwroot/demo-assets/tw-build.css",
    "build:scripts": "vite build",
    "build:assets": "npm run build:tailwind && npm run build:scripts"
},

Run the following to build the assets: npm run build:assets

Now, your React components are bundled and optimised alongside Umbraco's assets and will get our 'Hello Island!' example rendering in Umbraco wherever we put the island definition:

<div class="react-component" data-component="HelloIsland"></div>

Passing CMS Content into React Components

Let's take this to the next level by passing CMS content to our React components.

For this we are going to pass a second data attribute with any content as JSON.

In this example we are going to replace the Clean Starter Kit blog listing on the home page, with a React Island.

This will now be our HTML definition for the island:


<div class="react-component" data-component="BlogListing" data-content='@Html.Raw(contentJson)'></div>

You can compose your JSON data however you see fit in your existing Umbraco setup. For this simple demo example, we'll just do it in the Razor:


@{
    var content = new {
        articles = pageOfArticles.Select(x => new { x.Name, x.Subtitle })
    };
    var contentJson = Newtonsoft.Json.JsonConvert.SerializeObject(content);
}

You can find the above code in-situ in the latestArticlesRow.cshtml file in the repo here.

We can now extend our orchestrator to accept the content and pass to the React component as needed.


const elements = document.querySelectorAll(".react-component");

elements.forEach((element) => {
  // Get the component name from the data attribute
  const componentName = element.getAttribute("data-component");
  if (!componentName) {
    console.error('Attribute "data-component" is missing or null.');
    return;
  }

  const Component = componentsMap[componentName];
  if (!Component) {
    console.error(`Component "${componentName}" not found in componentsMap.`);
    return;
  }

  // Extract content props from data-content attribute
  const contentData = element.getAttribute("data-content");
  const content = contentData ? JSON.parse(contentData) : {};

  // Render the component and pass content props
  const root = createRoot(element);
  root.render(<Component {...content} />);
});

The full code above can be found in the repo here.

Within the BlogListing.tsx React component, we can then define TypeScript types for the incoming content:


interface Article {
    Name: string;
    Subtitle: string;
}

interface BlogListingProps {
    articles: Article[];
}

const BlogListing = ({ articles }: BlogListingProps): JSX.Element => {
    return (
      <div>
        {articles.map((article, index) => (
            <div>
                <h1>{ article.Name }</h1>
                <p> { article.Subtitle }</p>
            </div>
        ))}
      </div>
    );
};

export default BlogListing;

Now, your React component receives dynamic content directly from Umbraco. 🙌

Working Example

As mentioned above, there is a working example in the GitHub repo of the BlogListing.

In the interest of keeping it festive, the Blog Listing in the demo uses an Aceternity UI component to create and animated shooting star effect on the list cards ✨

Clean Starter Kit with Aceternity 'Meteors' React component used for the blog listing

Clean Starter Kit with Aceternity 'Meteors' React component used for the blog listing

In reality, you would only want to consider this for more complex use cases, maybe something with complex state or interactions, but it makes for a fun example.

Why Use React Islands in Umbraco?

So, why go to the effort of integrating React into Umbraco in this way?

Selective Enhancement

This approach allows you to create "islands" within your site - dedicated areas for interactive features like widgets, calculators, or dynamic forms - powered by React, while leaving the rest of the Umbraco framework intact. It’s an ideal solution for projects that don’t require a full separation of frontend and backend but still have specific needs for advanced interactivity.

Alternatives Considered

You might ask, “Why not stick with Vanilla JS or a lightweight library like Alpine.js?”

For straightforward interactions, those are valid and effective options. However, when your requirements include complex state management, component lifecycle methods, or tapping into the broader React ecosystem, React becomes the more advantageous choice.

Considerations

  • Performance: React adds weight to your JavaScript bundle. Use code-splitting with tools like Vite to load only what’s needed. For a lighter weight alternatives, you could consider libraries like Preact.
  • SEO: For critical content, ensure you include server-rendered HTML for React to hydrate. Use Razor to output static fallback content or consider SSR for key components.
  • Passing CMS Content: For demo purposes we set the JSON data in Razor. Try and do this in your Controller and for complex scenarios, you could even fetch data asynchronously via the Content Delivery API.
  • Error Handling: Build in graceful fallbacks for missing components or invalid data. Log errors clearly to assist debugging.
  • Accessibility: Pay attention to ARIA roles and keyboard navigation in your React components. Tools like Axe or Lighthouse can catch common issues.
  • Team Workflow: Define clear boundaries between your React and Umbraco teams to avoid conflicts. Shared conventions help when scaling.

Wrapping Up 🎁

This Islands Architecture-inspired approach offers the flexibility to integrate React-powered widgets or components where needed, while leaving the rest of your Umbraco site intact.

Although we’ve highlighted React here due to its popularity, this concept can just as easily be applied to other frameworks like Vue, Qwik, Solid, or any tool that fits your project’s requirements. The principles remain the same.

Hopefully, this serves as a useful example and sparks some ideas about how you might leverage this approach in your own projects. If you have suggestions for improving it or even better methods to share, I’d love to hear from you.

Wishing you all a fantastic Christmas! 🎄