920 Bonus: React islands [2d]

Posted . Visible to the public.

Most of MovieDB is server-rendered and enhanced with Unpoly, and that's our default. Sometimes one region of a page needs more interactivity than that model gives you comfortably. Then we build an island: a React component mounted into a server-rendered page. This card teaches the React basics you need for that — components, state, effects, talking to Rails — and how to mount an island from an Unpoly compiler. It is not about building single-page apps.

Important

Work on this lesson in advisor mode.

Learning goals

  • You can explain what a React island is and how it differs from a full single-page app.
  • You can write a React component with JSX, hold its state with useState, and render it from data the server passed in.
  • You can use useEffect to load data, react to changes and run periodic work, and you can explain why effects need cleanup.
  • You can let an island talk to Rails through a small JSON API, both reading and writing.
  • You can mount and unmount a React component from an Unpoly compiler so that fragment updates don't leak.
  • You can set up JSX compilation in an esbuild-based Rails app.

Setup

Do this yourself; it's part of the lesson.

  1. yarn add react react-dom
  2. Write components and the compiler in .jsx files. Alternatively add '.js': 'jsx' to the loader map in esbuild.config.js, so every JavaScript file may contain JSX.
  3. Add jsx: 'automatic' to the esbuild options, so you don't need import React from 'react' in every file.

That's all — esbuild compiles JSX natively; no Babel involved.

Mounting an island

The server renders a plain element with the data the island needs. An Unpoly compiler mounts the component and returns a destructor that unmounts it when the fragment goes away:

<div class="chat" data-room="3012"></div>
import ReactDOM from 'react-dom/client'
import ChatSession from './chat_session'

up.compiler('.chat', function(element, { room }) {
  const session = new ChatSession(room)
  const root = ReactDOM.createRoot(element)
  root.render(<Chat session={session} />)
  return () => root.unmount()
})

Resources

Read what's new to you, skim what's familiar, skip what you already master. Stop when you can meet the learning goals.

Your agent can also generate an overview, a tutorial or an explanation for anything here, tailored to what you already know. Just ask.

Exercises

1. Port the star rating to an island

In Agentic coding basics you built a 1-to-5-star rating with an average, server-rendered. Port it to a React island on the movie page: same feature, new architecture. When it works, compare: what got simpler, what got more complex?

Keep the state as a fixed set of slots — the hovered value, the user's own rating, the average, the count, whether a save is in progress, an error — no lists. Clicking a star saves the rating through a JSON endpoint; the response updates average and count.

2. Port the reviews to a live discussion island

Also in Agentic coding basics you built review texts for movies. Turn that list into a live discussion island: it loads the movie's reviews when it mounts and shows a loading state meanwhile, renders them as a flat list, lets the user post a new comment from a textarea, and polls for new comments every few seconds.

Clean up the polling interval when the component unmounts — this is what the compiler's destructor is for. No replies or threads.

Profile picture of Henning Koch
Henning Koch
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra Curriculum (2026-08-31 15:28)