DEV Community

Cover image for Implementing React Islands in static web applications
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Implementing React Islands in static web applications

Written by Nelson Michael✏️

React Islands gives us a practical approach to integrating React components into existing web applications. This architectural pattern enables teams to introduce modern React features into legacy codebases without requiring a complete rewrite, allowing for incremental modernization while maintaining system stability.

Think of it as dropping modern functionality into your older codebase without the headache of starting from scratch. It's perfect for teams looking to modernize gradually while keeping their apps stable and running smoothly.

In this guide, we'll walk through implementing React Islands step by step, using a real-world example. You'll learn how to:

  • Add React components to a vanilla JavaScript application
  • Pass data between React and your existing code
  • Handle state management across the boundary
  • Avoid common pitfalls and gotchas

When should you use React Islands?

The core concept of React Islands involves selectively hydrating specific sections of the page while leaving the rest static. This approach minimizes the JavaScript payload, resulting in faster page loads and reduced browser evaluation time. It’s particularly beneficial for slower connections or less powerful devices.

Before we dive in, let's talk about when this approach makes sense - and when it doesn't.

The good

React Islands allow you to gradually modernize your app, avoiding the need for a complete rewrite. You can introduce React incrementally, focusing on areas where you need it while leaving the rest of the application intact. This minimizes risk and disruption.

You can absolutely expect performance improvements with React Islands — because you’re only hydrating what's necessary, you get faster initial page loads. That’s all thanks to the burden lifted from not shipping a full React bundle for the entire page.

On the topic of hydration, let’s put some things into context. Traditionally, React hydration works by server-rendering the entire page. The client then loads React and rehydrates the entire page, making all components interactive at once. In contrast, the “Island Architecture” modifies this process by keeping most of the page static. Only specific components are hydrated, and hydration occurs independently for each island.

Finally, these islands help with SEO, seeing as static content is readily available for search engines to crawl.

The not-so-good

Let's talk about the challenges you'll face with React Islands. While it's powerful, there are some real gotchas to watch out for.

State management gets messy since the islands are isolated from each other – they're like separate mini-apps that can't easily share states or communicate. You'll need workarounds like custom events or global state management, which adds complexity. In another section, we’ll take a look at how a real-world implementation handles this.

Loading coordination is tricky because multiple islands might need the same dependencies. Without proper planning, you risk downloading duplicate code or running into race conditions where islands wait on each other's resources.

Layout shifts can ruin the user experience when your islands hydrate and change size. Your static HTML might render differently from the final interactive state, causing content to jump around during load.

Measuring performance becomes more nuanced — traditional metrics don't capture the full picture since different parts of your page become interactive at different times.

Perfect use cases

  • Content-heavy sites with minimal interactivity
  • Pages where most content is static
  • Scenarios where performance is crucial
  • Progressive enhancement requirements

Challenges in real-world implementation

In this article from The Guardian, they discuss their approach to integrating React Islands and some of the challenges they encountered during the process. Let’s highlight some of the key takeaways and insights from their experience:

1. Developer experience matters
While initial implementations often focus on performance metrics, The Guardian's experience shows that developer experience is crucial for long-term success. Their second implementation achieved performance goals but was challenging to maintain:

  • Adding new islands required changes in multiple locations
  • Developers could inadvertently introduce unused JavaScript
  • Code changes frequently reintroduced performance regressions

This led to their third implementation, which prioritized developer experience alongside performance, providing "guard rails" to help developers naturally write performant code.

2. State management expectations
One unexpected benefit from the isolation inherent in the islands' architecture? Simplified state management. By treating each island as a self-contained unit with a local state, codebases become easier to understand and maintain. This architectural constraint turned out to be a feature, not a bug.

Let’s address why "state management" takes a negative tone in the previous section but a positive one here. State isolation can be a benefit or a pitfall, depending on your architectural approach and requirements.

The Guardian succeeded because they leaned into isolation, accepting some duplication as a reasonable trade-off. However, if your application requires significant state sharing between components, this same isolation becomes a complex challenge that you need to overcome.

3. Data fetching considerations
Multiple islands often need the same data, potentially leading to duplicate API calls. Solutions to this challenge include:

  • Using data fetching libraries with built-in deduplication (like SWR)
  • Accepting some duplication of simple operations (like cookie reads)
  • Carefully planning data flow and caching strategies

Starting point: The legacy application

Let's start with a simple product catalog page. Here's our initial vanilla JavaScript version:

<body>
    <div class="container">
        <h1>Product Catalog</h1>
        <div id="search-container">
            <input type="text" id="searchInput" placeholder="Search products...">
        </div>
        <div id="product-list" class="product-list"></div>
    </div>
</body>
Enter fullscreen mode Exit fullscreen mode

Let's add some basic JavaScript functionality:

// Product data
const products = [
    { id: 1, name: "Laptop", price: 999.99 },
    { id: 2, name: "Smartphone", price: 699.99 },
    { id: 3, name: "Headphones", price: 199.99 }
];

// Render products
function renderProducts(productsToRender) {
    const productList = document.getElementById('product-list');
    productList.innerHTML = productsToRender.map(product => `
        <div class="product-card">
            <h3>${product.name}</h3>
            <p>${product.price}</p>
        </div>
    `).join('');
}

// Initial render
renderProducts(products);
Enter fullscreen mode Exit fullscreen mode

Adding your first React Island

Now comes the interesting part. Let's learn how to upgrade our search functionality to use React.

  1. First, add React to your page:

    <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
    
  2. Create your first React component:

    function SearchIsland({ onSearch }) {
        const [searchTerm, setSearchTerm] = React.useState('');
    
        const handleSearch = (event) => {
            const value = event.target.value;
            setSearchTerm(value);
            onSearch(value);
        };
    
        return (
            <div className="search-island">
                <input
                    type="text"
                    value={searchTerm}
                    onChange={handleSearch}
                    placeholder="Search products..."
                />
                <small>⚛️ React-powered search</small>
            </div>
        );
    }
    

    }

  3. Mount the component and handle communication:

    :function mountSearchIsland() {
    const searchContainer = document.getElementById('search-container');
    const handleSearch = (searchTerm) => {
        const filtered = products.filter(product =>
            product.name.toLowerCase().includes(searchTerm.toLowerCase())
        );
        renderProducts(filtered);
    };
    
    ReactDOM.render(
        <SearchIsland onSearch={handleSearch} />,
        searchContainer
    );
    
    }
    
    // Initialize
    mountSearchIsland();
    

Building bidirectional communication

Let's make things more interesting by adding a product selection that communicates both ways:

function ProductIsland({ product, onSelect }) {
    const [isSelected, setIsSelected] = React.useState(false);

    const handleClick = () => {
        setIsSelected(!isSelected);
        onSelect(product, !isSelected);
    };

    return (
        <div 
            className={`product-card ${isSelected ? 'selected' : ''}`}
            onClick={handleClick}
        >
            <h3>{product.name}</h3>
            <p>${product.price}</p>
            {isSelected && <span></span>}
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Update your rendering logic:

function renderProducts(productsToRender) {
    const productList = document.getElementById('product-list');
    productList.innerHTML = '';

    productsToRender.forEach(product => {
        const productContainer = document.createElement('div');
        ReactDOM.render(
            <ProductIsland 
                product={product}
                onSelect={(product, isSelected) => {
                    updateCart(product, isSelected);
                }}
            />,
            productContainer
        );
        productList.appendChild(productContainer);
    });
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

So, what have we learned? While it promises dramatic performance improvements, the reality is more nuanced. The Guardian's journey shows us that success isn't just about technical implementation — it's about embracing constraints and building around them.

Is React Islands the future of web development? Probably not on its own. But it’s part of a bigger picture in how we think about building performant web applications. It gives us another powerful tool in our toolbox. The key is understanding when to use it and, just as importantly, when not to.

Further Reading

Thanks for reading, and happy coding!


Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now

Top comments (0)