A coworker asked how React Islands work and I realized it's a technique I've been using to modernize monolithic web codebases for years, but never wrote down how it works.
What is a React Island
The React Island is a way to inject React components, even whole apps, into an otherwise statically rendered web project. It's a great way to start a strangler pattern rewrite, or it can be a long-term solution for when you need a highly interactive portion of your site, but want to keep everything else working the old way.
Back at React Summit 2022, I talked about this as the bottom-up approach to rewriting an app.
The core idea works like this:
- Render a bunch of HTML however you like
- Set up a root div to render your React into
- Call a function
- Render a small React app into that div
React then takes over rendering and interactivity inside that area. Any additional data gets loaded with API requests made from JavaScript in the browser. To save data you also make API requests or fake form submissions.
Why not just write all your code in React?
Because full rewrites never work. You'll end up in one of these situations:
A stagnating old system waiting for the overdue new system.
Or a new system that keeps chasing the old system and never quite has all the features you need.
You have to go piece by piece, ship continuously, and make sure you always keep software in a working state your users can play with. I promise you'll keep finding features you didn't know were there.
Why isn't all your code in 2024 already in React? Oh you'd be surprised how many successful businesses are out there built on duct tape and chewing gum from 2005. jQuery is still the world's most used javascript framework.
How a React Island works
There's a few parts:
- react components
- declaring islands
- building the code
- calling islands
React components
You build a React component as usual. Self-contained, takes care of fetching its own data, brings its own styling, all the good stuff.
function AmazingNewFeature({ prop1, prop2 }) {
const data = useQuery(...)
return <ReactStuff />
}
You can run this code however you like. It's a standard React component. Doesn't know anything about being an island. You can use TypeScript because once compiled it's Just Javascript.
Then you make a reactIslands.tsx
file that will declare your React islands. This file will become a separate build target.
Declaring islands
// All islands should share the query client
const queryClient = new QueryClient();
// App sets up all your global context providers
function App({ children }: PropsWithChildren) {
return (
<CssVarsProvider>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</CssVarsProvider>
);
}
function App({ children }: PropsWithChildren) {
return (
<CssVarsProvider>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</CssVarsProvider>
);
}
const ReactIslands = {
amazingNewFeature: function amazingNewFeature(
element: HTMLDivElement,
props: AmazingNewFeatureProps
) {
createRoot(element).render(
<App>
<AmazingNewFeature {...props} />
</App>
);
},
};
declare global {
interface Window {
ReactIslands: typeof ReactIslands;
mixpanel?: Mixpanel;
}
}
window.ReactIslands = ReactIslands;
You make an <App>
component that defines all your global providers. Styling, query cache, modals, etc. I always accumulate a bunch of these over time.
The important part is that any caching behavior (like React Query) is shared between all the islands. This will make your app feel faster and avoids losing context between islands. Makes it easy to share data, if you put multiple islands on the same page.
Building the code
These days I like to use Vite to build my JavaScript projects. Comes with good defaults, works fast, and means I never have to worry about the details.
You'll want to configure reactIslands.tsx
as its own build target so you have a clean file to include in your page. This file is going to pull in any other dependencies you declare through the usual import ...
approach.
build: {
rollupOptions: {
input: {
main: "./src/main.tsx",
reactIslands: "./src/reactIslands.tsx",
},
output: {
entryFileNames: "[name].js",
manualChunks(id) {
// Split vendor code into separate package
if (id.includes("node_modules")) {
return "vendor";
}
},
},
},
outDir: "dist",
},
I like to split vendor code into a separate package so the build artefacts are more stable and you don't need to bust cache as much.
You'll get a file called dist/reactIslands.js
. This is the one you include in your HTML.
Calling islands
Vite uses JavaScript modules, which means you don't need to worry about importing all of your JavaScript. Just the one entry file.
<script
type="module"
src="{{ url_for('static', filename='reactIslands.js') }}"
></script>
<script>
$(document).ready(function () {
window.ReactIslands.amazingNewFeature(
document.getElementById('amazing-new-feature'),
{
prop1: {{ value_from_server | tojson }},
prop2: {{ value_from_server | tojson }},
})
})
</script>
<div id="amazing-new-feature"></div>
Yes that's jQuery inside a Jinja template. The same approach will work with any server-side stack you use.
The key here is that we import the entry javascript file, which then pulls in the rest, and declare an area for React to render into. Then we call the island function and pass-in values from the server.
It's best to pass critical data into your island via props then fetch additional data inside the component itself. Where you make that distinction depends on your app.
So is this an MPA or an SPA?
Yes. This is both a Multi Page App and a Single Page App.
You get the MPA part from your existing infrastructure and the SPA part from the new React code. I've even put routers inside the React Island so we could make it jump to the right page regardless of where navigation happened.
And if you're wondering why not stick with "the simple approach that works" or even "bah humbug when did web development get so complicated" ... react islands are for when that stops working ❤️
Cheers,
~Swizec
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️