Bonus: React islands [2d]

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.

Henning Koch