Interactive blocks

Client Components that run in the browser, with or without Next.js, and what islands can't do.

A block can render Client Components: files that start with "use client", where any React hooks and event handlers work.

// blocks/like-button.tsx
"use client";

import { useState } from "react";

export function LikeButton({ label }: { label: string }) {
  const [likes, setLikes] = useState(0);
  return <button type="button" onClick={() => setLikes(likes + 1)}>{label} ({likes})</button>;
}
// goodfellow.config.tsx
import { LikeButton } from "./blocks/like-button";

export default defineConfig({
  blocks: {
    ...blocks,
    Like: { fields: { label: { type: "text" } }, render: ({ label }) => <LikeButton label={label} /> },
  },
});

They run in the browser however the site is built. With goodfellow build and goodfellow dev, each one a block uses becomes an island: its HTML is in the page as usual, and the page loads React and that component's code to bring it to life. Pages without Client Components load no JavaScript. With Next.js, Next.js runs them.

  • Props must be plain values: text, numbers, true and false, and lists and objects of these. A block can't pass a function such as an onClick handler, so handlers go inside the Client Component.
  • Content passed as children (or any prop holding JSX) is rendered by the block, and stays as it is in the browser. Client Components in it run on their own.
  • useSite() works in Client Components, so pages with them include the site's settings, menus and collections for the browser.
  • Other packages' Client Components need re-exporting from a "use client" file of the site's: "use client"; export { Carousel } from "some-carousel";. Packages of blocks (packages that use @goodfellow-cms/react) don't.

Each page with a Client Component loads React, so use HTML and CSS where they're enough: <details> for something that opens and closes, :hover and :focus-within for menus.

What islands can't do

Islands have limits that Next.js doesn't:

  • No shared context. Each island is its own React app, so a Client Component that provides a context can't pass it to Client Components in the content it wraps. Components whose parts share a context, such as an accordion and its items, go together in one "use client" file.
  • Content is fixed HTML. A Client Component can show, hide or move its children, but can't look inside them with React.Children or cloneElement.
  • Fewer kinds of props. No dates, Maps, Sets or promises.
  • Every page is a full page load, so React starts again on each page.

If a site runs into these, build it with Next.js instead.