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:
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
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:
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:
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>
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);
Now comes the interesting part. Let’s learn how to upgrade our search functionality to use React.
<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>
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> ); }
}
: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();
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> ); }
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); }); }
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.
Thanks for reading, and happy coding!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowOnlook bridges design and development, integrating design tools into IDEs for seamless collaboration and faster workflows.
JavaScript generators offer a powerful and often overlooked way to handle asynchronous operations, manage state, and process data streams.
webpack’s Module Federation allows you to easily share code and dependencies between applications, helpful in micro-frontend architecture.
Whether you’re part of the typed club or not, one function within TypeScript that can make life a lot easier is object destructuring.