Emmanuel Akinfulubi

Starting out all 3 written

What React is, which build of it every panel here runs, and how to read the first error you get.

1 What React is, and what it is not

React is a library for describing what a screen should look like, as a function of your data. You write the description; React works out the changes. That is the whole idea, and this lesson shows the objects it is made of.

The build every panel runs. Every panel runs React's development build unless its badge says React prod; lesson 2 shows how and why. The development build is the one that tells you what you did wrong, which is why a course about reading React's messages uses it by default. Every panel's badge says which one it ran, and the output label repeats it.

An element is a plain object

Calling createElement does not touch a screen. It returns an object that says "a p with the text hi". The panel imports it the way any ES module does; the Node.js course covers import if it is new to you.

An element, printed

ran at build time · server (no DOM) · React dev

element.mjs

import { createElement } from 'react';

const el = createElement('p', null, 'hi');

console.log(typeof el);
console.log(String(el.$$typeof));
console.log(el.type);
console.log(Object.keys(el));
console.log(el.props);

check('an element is an object', typeof el, 'object');
check('it is a plain object', Object.getPrototypeOf(el) === Object.prototype, true);
check('its marker is a symbol', String(el.$$typeof), 'Symbol(react.transitional.element)');
check('its type is what we asked for', el.type, 'p');
check('its children are in props', el.props.children, 'hi');
check('the development build freezes it', Object.isFrozen(el) && Object.isFrozen(el.props), true);

What node printed (React 19.2.7, development build), when the build ran it

node element.mjs

object
Symbol(react.transitional.element)
p
[ '$$typeof', 'type', 'key', 'props', '_owner', '_store' ]
{ children: 'hi' }
ok   an element is an object
ok   it is a plain object
ok   its marker is a symbol
ok   its type is what we asked for
ok   its children are in props
ok   the development build freezes it

Nothing here is special: a type, some props, a key. The marker $$typeof is a symbol so that JSON coming from a server cannot pretend to be an element. The object is frozen because it is a description, not a thing you change; to change what is on the screen you describe it again.

A component is a function that returns elements

A component is an ordinary function: it takes an object of props and returns elements. The tree of elements it returns is still only a description. Turning that description into HTML is the job of a renderer, and renderToString from react-dom/server is the smallest one.

A component, rendered to HTML

ran at build time · server (no DOM) · React dev

greeting.mjs

import { createElement } from 'react';
import { renderToString } from 'react-dom/server';

function Greeting({ name }) {
  return createElement('h1', null, 'Hello, ', name);
}

const html = renderToString(createElement(Greeting, { name: 'Ada' }));
console.log(html);

check('the HTML React wrote', html, '<h1>Hello, <!-- -->Ada</h1>');

What node printed (React 19.2.7, development build), when the build ran it

node greeting.mjs

<h1>Hello, <!-- -->Ada</h1>
ok   the HTML React wrote

The <!-- --> between Hello, and Ada is React's doing. Two text children next to each other would be read back by a browser as one text node, so React puts a comment between them to keep them apart. You will meet it again when a page is hydrated, in lesson 32.

What React is not

React draws the screen and nothing else. It has no router, no way of fetching data, no build step and no opinion about your server. Those come from other tools, and later lessons show where each one plugs in. What it does give you is the same model on the server and in the browser, which is why one version of the library renders HTML here and, in lesson 8, a live DOM.

Which React this is

Which React, and which build

ran at build time · server (no DOM) · React dev

version.mjs

import * as React from 'react';

console.log(React.version);
console.log(typeof process.env.NODE_ENV);
console.log(typeof React.act);

check('the React in this course', React.version, '19.2.7');
check('a default panel sets no build variable', typeof process.env.NODE_ENV, 'undefined');
check('and so it runs the development build, which has act', typeof React.act, 'function');

What node printed (React 19.2.7, development build), when the build ran it

node version.mjs

19.2.7
undefined
function
ok   the React in this course
ok   a default panel sets no build variable
ok   and so it runs the development build, which has act

React decides which of its two builds to load by reading process.env.NODE_ENV when it is first imported. Nothing sets it in a default panel, so it is undefined, and React takes the development build. The production build has no act, which is one of the differences lesson 2 shows side by side.

2 Running React here: Node, the preload, the sandbox, and dev vs production builds

Before the first component, it is worth knowing exactly what runs it. Every panel in this course is executed by the Node on the machine that built these pages, against a copy of React that sits in the course's own vendor folder. This lesson shows the two small files that make that safe and repeatable, and then runs the same code against both builds of React.

What a panel runs with

A panel is a few files and one command, node App.jsx or similar. Around it the build adds scaffolding, and the scaffolding is kept out of the panel's own files so that a line number in an error is the line number you see on the page. It is two files, shown below as the course's own tools. They are not panels: nothing here is run as a lesson, and no panel ever contains them.

tools/preload.cjs is loaded into every Node process with node --require. It compiles .jsx files with the TypeScript that is vendored next to React, defines the check() function the panels call, records the console warnings a panel prints so the build can compare them with what the panel declared, gives DOM panels a window, refuses a server that listens on anything but loopback, and notes which build of React was loaded. It contains no path: it reads what it needs from six environment variables, COURSE_VENDOR_ROOT, COURSE_SCRATCH, COURSE_SIDE, COURSE_DOM, COURSE_ACT and, for production panels, NODE_ENV.

tools/preload.cjs

shown, not run: the course's own tool
/* The course's preload. Every panel runs with `node --require <this file>`, and every
   `node` a terminal panel starts gets it through NODE_OPTIONS. It is the scaffolding
   around a panel, kept out of the panel's own files so that line numbers in a stack
   trace are the line numbers you see on the page.

   It reads everything it needs from the environment and contains no path:

     COURSE_VENDOR_ROOT   the vendored node_modules (React, TypeScript, happy-dom)
     COURSE_SCRATCH       the directory the panel lives in
     COURSE_SIDE          a file the panel's warnings and build records are appended to
     COURSE_DOM           "1": give the panel a DOM (happy-dom)
     COURSE_ACT           "1": tell React it is running under act()
     NODE_ENV             "production" selects React's production build

   Eight steps, in this order. */
'use strict';

const fs = require('node:fs');
const path = require('node:path');
const util = require('node:util');
const net = require('node:net');
const nodeModule = require('node:module');
const { fileURLToPath } = require('node:url');

const VENDOR_ROOT = process.env.COURSE_VENDOR_ROOT;
const SCRATCH = process.env.COURSE_SCRATCH;
const SIDE = process.env.COURSE_SIDE;
if (!VENDOR_ROOT) throw new Error('course: COURSE_VENDOR_ROOT is not set');
if (!SCRATCH) throw new Error('course: COURSE_SCRATCH is not set');
if (!SIDE) throw new Error('course: COURSE_SIDE is not set');

/* One line of JSON per record. Synchronous, so a record written just before the
   process dies is still there. */
function record(level, text) {
  fs.appendFileSync(SIDE, JSON.stringify({ level, text }) + '\n');
}

const inside = (file, root) => file === root || file.startsWith(root + path.sep);

/* 1. Stack traces name the lines of the file as written, not of the compiled code. */
nodeModule.setSourceMapsSupport(true);

/* 2. Two hooks.
      resolve: nothing may be loaded from outside the vendored tree or the panel's own
               directory. Node's built-ins pass.
      load:    .jsx files are compiled by the vendored TypeScript, with an inline source
               map. Every .jsx file is an ES module. Everything else passes untouched. */
let ts = null;
let jsxOptions = null;

function compiler() {
  if (!ts) ts = nodeModule.createRequire(path.join(VENDOR_ROOT, 'noop.js'))('typescript');
  if (!jsxOptions) jsxOptions = JSON.parse(fs.readFileSync(path.join(__dirname, 'jsx.json'), 'utf8'));
  return ts;
}

function compileJsx(file, source) {
  const tsc = compiler();
  const name = path.basename(file);
  const options = process.env.NODE_ENV === 'production' ? jsxOptions.production : jsxOptions.development;
  const result = tsc.transpileModule(source, {
    fileName: name,
    compilerOptions: options,
    reportDiagnostics: true,
  });
  const diagnostics = result.diagnostics || [];
  if (diagnostics.length) {
    const first = diagnostics.reduce((a, b) => ((b.start || 0) < (a.start || 0) ? b : a));
    const line = first.file ? first.file.getLineAndCharacterOfPosition(first.start || 0).line + 1 : 1;
    const text = `${name}:${line}: ${tsc.flattenDiagnosticMessageText(first.messageText, ' ')}`;
    /* The record comes first: the verdict fails the panel on it, so a compile error
       can never pass as a panel that declared it would die with a SyntaxError. */
    record('jsx', text);
    const error = new SyntaxError(text);
    error.code = 'ERR_COURSE_JSX';
    throw error;
  }
  return result.outputText;
}

nodeModule.registerHooks({
  resolve(specifier, context, nextResolve) {
    const resolved = nextResolve(specifier, context);
    const url = resolved.url;
    if (!url || !url.startsWith('file:')) return resolved;
    const file = fileURLToPath(url);
    if (inside(file, VENDOR_ROOT) || inside(file, SCRATCH)) return resolved;
    /* Only names, never the directories they sit in: where this machine keeps node, npm
       or the project is nobody's business but its owner's. */
    const name = (p) => (path.isAbsolute(p) ? path.basename(p) : p);
    const error = new Error(`course: ${name(specifier)} resolved outside the vendored tree, to ${path.basename(file)}`);
    error.code = 'ERR_COURSE_UNVENDORED';
    throw error;
  },
  load(url, context, nextLoad) {
    if (url.startsWith('file:') && /\.jsx(?:[?#].*)?$/.test(url)) {
      const file = fileURLToPath(url.replace(/[?#].*$/, ''));
      const source = compileJsx(file, fs.readFileSync(file, 'utf8'));
      return { format: 'module', source, shortCircuit: true };
    }
    return nextLoad(url, context);
  },
});

/* 3. check(name, got, want): the assertion every panel uses. It prints "ok   name" or
      "FAIL name  got ..., want ...". Deep equality, so a Map is not {} and NaN is not
      null, which a JSON.stringify comparison would claim. */
const { isDeepStrictEqual, inspect } = require("node:util");

globalThis.check = function check(name, got, want) {
  const ok = isDeepStrictEqual(got, want);
  console.log((ok ? "ok   " : "FAIL ") + name + (ok ? "" : "  got " + inspect(got) + ", want " + inspect(want)));
};

/* 4. Warnings. console.error and console.warn still print exactly what they printed;
      each call is also recorded, so the build knows which warnings a panel produced. */
for (const level of ['error', 'warn']) {
  const original = console[level];
  console[level] = function (...args) {
    record(level, util.format(...args));
    return Reflect.apply(original, this, args);
  };
}

/* 5. A DOM, when the panel asks for one: a happy-dom window installed as the globals a
      browser would have, and an empty region to render into, available as `stage`. It
      loads no scripts, styles or frames. */
function defineGlobal(name, value) {
  Object.defineProperty(globalThis, name, { value, configurable: true, writable: true, enumerable: false });
}

if (process.env.COURSE_DOM === '1') {
  const { Window } = nodeModule.createRequire(path.join(VENDOR_ROOT, 'noop.js'))('happy-dom');
  const window = new Window({
    url: 'http://localhost/',
    settings: {
      disableJavaScriptEvaluation: true,
      disableJavaScriptFileLoading: true,
      disableCSSFileLoading: true,
      disableIframePageLoading: true,
    },
  });
  const names = [
    'window', 'document', 'navigator', 'Node', 'Element', 'Text', 'MutationObserver',
    'Event', 'MouseEvent', 'KeyboardEvent', 'InputEvent', 'FocusEvent', 'SubmitEvent', 'FormData',
    ...Object.getOwnPropertyNames(window).filter((n) => /^(?:HTML|SVG)\w*Element$/.test(n)),
  ];
  for (const name of names) {
    if (name === 'window') defineGlobal(name, window);
    else if (window[name] !== undefined) defineGlobal(name, window[name]);
  }
  defineGlobal('requestAnimationFrame', window.requestAnimationFrame.bind(window));
  defineGlobal('cancelAnimationFrame', window.cancelAnimationFrame.bind(window));

  const region = window.document.createElement('div');
  region.setAttribute('id', 'stage');
  region.setAttribute('role', 'region');
  region.setAttribute('aria-label', 'Rendered output');
  window.document.body.appendChild(region);
  defineGlobal('stage', region);

  if (process.env.COURSE_ACT === '1') globalThis.IS_REACT_ACT_ENVIRONMENT = true;

  /* 6. Clean up when the process ends through process.exit(). A timer the panel left
        running keeps the process alive, so it never reaches here: unmount your roots and
        clear your timers. */
  process.on('exit', () => {
    try { window.happyDOM.abort(); } catch { /* already closed */ }
    try { window.close(); } catch { /* already closed */ }
  });
}

/* 7. Servers listen on this machine only. The sandbox already stops anything leaving it;
      this stops a panel from listening on every interface. A Unix socket path is fine. */
const LOOPBACK = new Set(['127.0.0.1', 'localhost', '::1']);
const originalListen = net.Server.prototype.listen;

net.Server.prototype.listen = function listen(...args) {
  const first = args[0];
  let host;
  let socketPath = false;
  if (first !== null && typeof first === 'object') {
    if (typeof first.path === 'string') socketPath = true;
    else host = first.host;
  } else if (typeof first === 'string' && !/^\d+$/.test(first)) {
    socketPath = true;
  } else if (typeof args[1] === 'string') {
    host = args[1];
  }
  if (!socketPath && !LOOPBACK.has(host)) {
    const error = new Error(
      `course: listen on ${host === undefined ? 'every interface' : host} is refused; a panel's server listens on 127.0.0.1`,
    );
    error.code = 'ERR_COURSE_NOT_LOOPBACK';
    throw error;
  }
  return Reflect.apply(originalListen, this, args);
};

/* 8. Which React build ran. When the process ends, every React file that was loaded
      and evaluated is written down as "development: react/cjs/react.development.js" or
      "production: ...". The build compares that with the build the panel declared, so
      a panel cannot print a "production" badge while running the development build. */
const REACT_BUILD = /\/node_modules\/((?:react|react-dom|scheduler)\/cjs\/[^/]+\.(development|production)\.js)$/;

process.on('exit', () => {
  for (const [file, mod] of Object.entries(require.cache)) {
    const m = REACT_BUILD.exec(file);
    if (m && mod && mod.loaded === true) record('build', `${m[2]}: ${m[1]}`);
  }
});

The scaffolding every panel runs with, shown here once as a tool of the course and never as part of a panel. It is the file on disk, byte for byte, so it is the one that ran.

tools/sandbox.sb is a profile for sandbox-exec, the macOS tool that runs a program with limits. Every panel runs inside it, and so does each terminal command. The profile leaves a program alone except for the network: it may connect to and listen on the machine it is on (loopback), and nothing else. Node panels also run under --permission, so they cannot read or write outside their own folder, the vendored tree and the preload. Before any panel runs, the build tries three times to reach an address on the internet from inside the wall and refuses to continue unless all three are blocked; the result is the last line of the figures on the contents page.

tools/sandbox.sb

shown, not run: the course's own tool
(version 1)
(allow default)
(deny network-outbound)
(allow network-outbound (remote ip "localhost:*"))
(deny network-bind)
(allow network-bind (local ip "localhost:*"))

The profile every panel runs under, shown here once as a tool of the course. Apple has deprecated sandbox-exec but it still ships with macOS, and it is what makes "no network" a fact about the build and not a promise.

Two builds of one library

React ships two builds in one package. The development build checks what you do and writes warnings; the production build is smaller and faster and says very little. Which one loads is decided once, when React is first imported, by reading process.env.NODE_ENV. A panel that does not say otherwise runs the development build, and its badge reads React dev. A panel declared as production sets NODE_ENV=production, and its badge reads React prod.

A terminal panel is a real shell session, so the build shows how it gets the same choice. The next panel runs one small file with node. The preload reaches that node through the environment, which is why check() exists in a file with no import for it.

A terminal, development build

ran at build time · terminal · React dev

build.cjs

const React = require('react');
const production = process.env.NODE_ENV === 'production';

console.log('the build variable is', process.env.NODE_ENV);
console.log('typeof React.act is', typeof React.act);

check('the variable matches the build', process.env.NODE_ENV, production ? 'production' : undefined);
check('act exists only in the development build', typeof React.act, production ? 'undefined' : 'function');

The terminal (React 19.2.7, development build), as the build ran it

node build.cjs
the build variable is undefined
typeof React.act is function
ok   the variable matches the build
ok   act exists only in the development build

NODE_OPTIONS loads the course preload into every node this terminal runs, so each one records which React build it loaded.

Now the same file, unchanged, in a terminal declared as production. The first command in the transcript was not typed by the lesson: the course adds export NODE_ENV=production to a production terminal, shows it, and runs it like any other command, so what you read is what ran.

The same file, production build

ran at build time · terminal · React prod

Also on disk, exactly as printed in an earlier panel: build.cjs

The terminal (React 19.2.7, production build), as the build ran it

export NODE_ENV=production
node build.cjs
the build variable is production
typeof React.act is undefined
ok   the variable matches the build
ok   act exists only in the development build

NODE_OPTIONS loads the course preload into every node this terminal runs, so each one records which React build it loaded.

The first command was added by the course because this panel runs React's production build; NODE_ENV is in the environment of every command after it.

The file did not change and the answers did, because React chose the other build. The production build has no act, which is the tool tests use to flush React's work, so a test must run against development.

The output is the same where it should be

What React writes for the same tree does not depend on the build. The next panel is declared production and renders the greeting from lesson 1.

The greeting, in production

ran at build time · server (no DOM) · React prod

greeting.mjs

import { createElement } from 'react';
import { renderToString } from 'react-dom/server';

function Greeting({ name }) {
  return createElement('h1', null, 'Hello, ', name);
}

const html = renderToString(createElement(Greeting, { name: 'Ada' }));
console.log(html);
console.log(process.env.NODE_ENV);

check('production wrote the same HTML as development', html, '<h1>Hello, <!-- -->Ada</h1>');
check('and this panel really ran the production build', process.env.NODE_ENV, 'production');

What node printed (React 19.2.7, production build), when the build ran it

NODE_ENV=production node greeting.mjs

<h1>Hello, <!-- -->Ada</h1>
production
ok   production wrote the same HTML as development
ok   and this panel really ran the production build

What changes is everything around the output: the warnings, the error messages and the speed. Later lessons show warnings that exist only in development, and the error catalogue shows what each message looks like.

On your own machine

You will not copy any of the above. On your own machine React lives in a project, built by a tool such as Vite, and npm installs it. The Node.js course covers running code and npm, which is all the machinery a React project sits on.

A React project on your own machine

shown, not run
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev

needs Vite and the npm registry, which this offline course never contacts. The panels above run the same React without them.

3 Your first component, and reading React's errors

A component is a function that returns elements. You can build a whole application from nothing else. This lesson writes one, with JSX, renders it, and then breaks it on purpose to read what React says.

A component with props

The file below is App.jsx. The tags look like HTML, and they are not: lesson 4 shows that each one is a function call. For now, read it as a description of the page. A component's name starts with a capital letter, which is how React tells <Badge /> (your function) from <p> (an HTML element). Whatever you write as attributes arrives as one object, the props.

A component and its props

ran at build time · server (no DOM) · React dev

App.jsx

import { renderToString } from 'react-dom/server';

function Badge({ name, role }) {
  return (
    <p>
      <strong>{name}</strong> is a {role}
    </p>
  );
}

const html = renderToString(<Badge name="Ada" role="mathematician" />);
console.log(html);

check('the HTML React wrote', html, '<p><strong>Ada</strong> is a <!-- -->mathematician</p>');
check('the same component with other props', renderToString(<Badge name="Grace" role="admiral" />),
  '<p><strong>Grace</strong> is a <!-- -->admiral</p>');

What node printed (React 19.2.7, development build), when the build ran it

node App.jsx

<p><strong>Ada</strong> is a <!-- -->mathematician</p>
ok   the HTML React wrote
ok   the same component with other props

The curly braces are where JavaScript goes: {name} is the value of the name prop, not the word. The same function produced both results, which is the point of a component: one description, many uses.

Reading an error

You are going to see this within the hour, so here it is on purpose. Profile reads user.profile.name, and the user it is given has a name but no profile. The panel is declared to die with a TypeError, and the build checks that it did. Read it in this order.

An error, two components deep

dies with TypeError · server (no DOM) · React dev

main.jsx

import { renderToString } from 'react-dom/server';

// A small team page.
function Avatar({ src, alt }) {
  return <img src={src} alt={alt} width="40" height="40" />;
}

function Header({ title }) {
  return (
    <header>
      <h1>{title}</h1>
    </header>
  );
}

// Shows one person.
function Profile({ user }) {
  return <p>{user.profile.name}</p>;
}

function Page() {
  return (
    <main>
      <Header title="Team" />
      <Profile user={{ name: 'Ada' }} />
    </main>
  );
}

try {
  renderToString(<Page />);
} catch (e) {
  const frames = e.stack.split('\n').filter((line) => line.startsWith('    at '));
  const line = Number(/main\.jsx:(\d+)/.exec(e.stack)[1]);
  check('the error points at line 18 as shown', line, 18);
  check('and the first frame is the component', /^ {4}at Profile /.test(frames[0]), true);
  throw e;
}

What node printed as it died (React 19.2.7, development build), when the build ran it

node main.jsx

ok   the error points at line 18 as shown
ok   and the first frame is the component
/app/main.jsx:18
  return <p>{user.profile.name}</p>;
                          ^

TypeError: Cannot read properties of undefined (reading 'name')
    at Profile (/app/main.jsx:18:27)
    at Object.react_stack_bottom_frame (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:9808:18)
    at renderWithHooks (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5062:19)
    at renderElement (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5497:23)
    at retryNode (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6428:21)
    at renderNodeDestructive (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6367:11)
    at renderNode (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6905:18)
    at renderChildrenArray (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6674:11)
    at retryNode (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6441:13)
    at renderNodeDestructive (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6367:11)

Node.js v26.7.0
  1. The first line is a place. /app/main.jsx:18, then that line of your source with a ^ under the exact spot. The file is the one above, and the line number is the line number you can count in it: the build compiles the JSX with source maps on, so the compiled code that actually ran never shows. The panel checks that, so it is not a claim.
  2. The next line is the problem. TypeError: Cannot read properties of undefined (reading 'name'). Something tried to read name from undefined. Here that is user.profile, which does not exist.
  3. The first at line is where it happened. at Profile (/app/main.jsx:18:27): your component, line 18, column 27. The panel checks that too.
  4. Everything after that is React. The frames under /app/node_modules/react-dom are the renderer walking the tree. Stop reading when you reach them. The mistake is almost never there.

The error says where it was noticed. The mistake is in the data one level up: Page gave Profile a user without a profile. That gap, between where a program noticed a problem and where it was caused, is what most debugging is.

Reading a warning

Not everything React says kills the render. A warning is printed and the program carries on, and in a browser it goes to the console. This one is about HTML: a <div> may not sit inside a <p>. The panel declares that it will warn, and the build checks that the warning appeared and nothing else did.

A warning, with its component stack

warns, on purpose · in happy-dom 20.11.6 · React dev

App.jsx

import { act } from 'react';
import { createRoot } from 'react-dom/client';

function Card() {
  return (
    <p>
      <div>inside</div>
    </p>
  );
}

const root = createRoot(stage);
await act(async () => root.render(<Card />));
check('it rendered anyway', stage.innerHTML, '<p><div>inside</div></p>');
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node App.jsx

In HTML, <div> cannot be a descendant of <p>.
This will cause a hydration error.

  <Card>
>   <p>
>     <div>

ok   it rendered anyway

A warning is a sentence, then a component stack: the part of the tree around the mistake, outermost first, with > marking the path to the offending element. Read it the way you read the frames above. Find the component you wrote, here <Card>, and the element inside it that React is pointing at. React adds that this will cause a hydration error because an HTML parser would not keep that nesting; lesson 32 is about hydration. On the client React built exactly what the JSX describes, and the check confirms it.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.