Skip to content
learn

Journal

  • React 19.3.0, Vite 8.3.0, TypeScript 7.0.2, Vitest 5.0.0, Testing Library and oxlint pinned in the course’s own package.json and lock file
  • check.sh: create-vite, the dev server, an HMR update, tsc, vite build, oxlint, every error snippet and every test, compared with expected/
  • CI on three OSes: green since 2026-09-16 (see below)
  • Lesson 1: a Vite project
  • Lesson 2: components and JSX
  • Lesson 3: state and rendering
  • Lesson 4: events and forms
  • Lesson 5: effects

React 19.3.0, Vite 8.3.0, TypeScript 7.0.2 and Vitest 5.0.0, pinned in the course’s own lock file. The first ten rows are the toolchain; the last two are GuitarAlchemist/ga at 8cc8c5a, and, as the dated entry says, nothing there has been reported to GA.

There is no Experiments table: every entry is a discovery written afterwards. The three to verify notes in this journal name what has not been measured; none of them predicts what the measurement will show.

Expected What happens Where Measure Status
Vite strips TypeScript types with esbuild Vite 8 bundles with Rolldown and transforms with Oxc; @vitejs/plugin-react 6 has no Babel dependency, and Fast Refresh is done by Oxc too Vite 8.3.0, @vitejs/plugin-react 6.1.1 Read in the installed packages By design, a changed toolchain. The TypeScript course’s sentence about esbuild describes GuitarAlchemist/ga, which is on Vite 5, and stays true there 2026-09-15
The dev server prints the same banner whether or not stdout is a terminal With a non-TTY stdout Vite leaves out press h + enter to show help, so a script waiting for that line waits forever Vite 8.3.0, scripts/dev-start.mjs The first script never saw the line and left a server running on port 5199 Reproduced; the script now waits for use --host to expose 2026-09-15
server.close() on a dev server created through the JS API resolves after the last request On Windows it never settles: the first module request started the dependency optimizer and left it running Vite 8.3.0, Node.js 24.21.0, Windows, scripts/dev-module.mjs Detected unsettled top-level await, and the process exits with code 13 Reproduced, not reported; waitForRequestsIdle() before close() fixes it. Linux and macOS not checked, and Vite’s tracker not searched 2026-09-15
--base /learn/react-vite/ reaches Vite as written Git Bash rewrites any argument that starts with / into a Windows path Git Bash (MSYS) on Windows, check.sh The built pages asked for /Program Files/Git/learn/react-vite/assets/… By design in MSYS; MSYS_NO_PATHCONV=1 and module paths without a leading slash fix it 2026-09-15
Testing Library unmounts what a test rendered, after each test Only when the test framework exposes a global afterEach; Vitest does not unless globals: true @testing-library/react 16.3.3, Vitest 5.0.0 One test’s DOM stayed into the next, and queries found two forms By design, and documented; setup.ts calls cleanup in afterEach 2026-09-15
A test reporter attributes console output to the test that printed it Vitest’s default reporter groups console output by timing, not by test, and the grouping changed between runs Vitest 5.0.0 Two runs of the same file grouped the same logs differently Reproduced; a small custom reporter collects each log with its test, and check.sh runs one file per Vitest run 2026-09-15
Vitest isolates tests, so module-level state resets between them It isolates test files; a module-level counter kept counting from one test to the next Vitest 5.0.0, lesson 3’s first StrictMode test The counter carried over By design; the test now passes the mutated array as a prop 2026-09-15
FormEvent and FormEventHandler are the types for React form handlers @types/react 19.3.0 marks both @deprecated and points to ChangeEvent, InputEvent and SubmitEvent @types/react 19.3.0 The deprecation comment says “FormEvent doesn’t actually exist”; ChangeEvent now takes two type parameters By design; lesson 4 uses SubmitEvent<HTMLFormElement> 2026-09-15
A component’s return type is ReactNode The 19.3 types accept async components, for Server Components TypeScript 7.0.2, @types/react 19.3.0 tsc prints Promise<ReactNode> | ReactNode in its message By design 2026-09-15
StrictMode’s double render on mount is invisible in the DOM An impure component renders twice and the DOM keeps the second render’s output React 19.3.0, StrictMode Played so far: G G By design, and what lesson 3 uses to make an impurity visible 2026-09-15
A token that can push code can push a workflow file GitHub also requires the workflow scope to create a file under .github/workflows GitHub, the push of react-vite-examples.yml The first push was refused Worked around by pushing the code without the workflow. The workflow reached main later: three runs on 2026-09-16 failed, then run 35098302366, at cd98475, passed on Ubuntu, Windows and macOS 2026-09-15
An expanded row stays expanded when the list is filtered or polled DynamicPanel stores expanded rows as positions in the filtered, polled data, so any reorder expands a different row GuitarAlchemist/ga at 8cc8c5a, DynamicPanel.tsx, lines 76-120 Reproduced in the course’s ExpandableList.tsx; the panel’s rows are unknown[], with no identity field known Fixed upstream by #733, merged 2026-09-24: rows are remembered by identity (id, else key, else the title field) 2026-09-15, 2026-09-24
A child reporting its value to its parent from an effect settles after one round NotesSelector calls onNotesChange from an effect that depends on it, and ScaleSelector passes a new handler at every render that stores a new array GuitarAlchemist/ga at 8cc8c5a, NotesSelector.tsx 15-18 and ScaleSelector.tsx 18-21 The course’s reduction in lesson 4 ends in Maximum update depth exceeded. Latent in GA: no application at that commit renders ScaleSelector Reproduced in GA’s own ScaleSelector by #733’s test, and fixed by it 2026-09-15, 2026-09-24
  • npm view gives React and React DOM 19.3.0, released on 9 September 2026, Vite 8.3.0, @vitejs/plugin-react 6.1.1, Vitest 5.0.0, jsdom 30.0.1, @testing-library/react 16.3.3, @testing-library/dom 10.4.2, @testing-library/user-event 14.6.7, oxlint 1.83.0 and create-vite 9.2.1. The course pins them exactly in code/react-vite/package.json, and uses Node.js 24.21.0 and TypeScript 7.0.2, like the site’s other JavaScript courses.
  • create-vite 9.2.1’s react-ts template asks for TypeScript ~6.0.2, not 7.0: a range with ~ allows 6.0.x only. The course uses 7.0.2, and tsc -b checks the template’s two projects without a change. Its strict is absent, because it has been the default since TypeScript 6.0, and its lint tool is oxlint rather than ESLint, with an --eslint option to get ESLint back.
  • Vite 8 bundles with Rolldown and transforms with Oxc. The TypeScript course’s lesson 1 says that Vite strips types with esbuild: that is true of GA’s Vite 5, and no longer of Vite 8. @vitejs/plugin-react 6 has no Babel dependency either; Fast Refresh is done by Oxc.
  • The lock file written on Windows lists the native packages of every platform as optional dependencies: @rolldown/binding-*, @oxlint/* and @typescript/typescript-*.
  • The dev server, the modules it serves and the HMR message are captured by scripts, not by hand: dev-start.mjs starts vite and stops it when the URL is printed, dev-module.mjs requests one URL through Vite’s JavaScript API, and hmr.mjs connects to the HMR WebSocket, edits a copy of App.tsx and prints the messages.
  • When the output isn’t a terminal, Vite prints no press h + enter to show help line. My first dev-start.mjs waited for that line, never saw it, and left a server running on port 5199; it now waits for use --host to expose.
  • On Windows, server.close() called right after the first module request never settled: Node.js printed “Detected unsettled top-level await” and exited with code 13. The request had started the dependency optimizer, which was still running. Awaiting server.environments.client.waitForRequestsIdle() before close() fixed it. I haven’t checked whether Linux or macOS behave the same (to verify), nor looked for an existing Vite issue.
  • Git Bash on Windows rewrites arguments that start with /: vite build --base /learn/react-vite/ built pages that asked for /Program Files/Git/learn/react-vite/assets/…. check.sh sets MSYS_NO_PATHCONV=1, and the scripts take module paths without their leading slash.
  • The outputs are normalized by normalize.mjs: durations, the ?v= hash of pre-bundled dependencies, HMR timestamps and inline source maps. The production file names, such as index-BQ_Vbf-Q.js, are content hashes and are kept as they are.
  • Fast Refresh keeping state is not in check.sh, which only sees the WebSocket message. I checked it by hand in a browser: three clicks on the counter, two on the capo, then an edit of the heading in App.tsx. The heading changed, the counter and the capo kept their values, and a variable set on window before the edit survived, so the page hadn’t reloaded. For lesson 1, I also served the build with a type error with vite preview and clicked its button twice: Count is 01, then Count is 011.
  • React Testing Library unmounts rendered components after each test only when the test framework exposes a global afterEach. Vitest doesn’t, unless globals: true is set, so the DOM of one test stayed in the next and queries found two forms. src/testing/setup.ts calls cleanup in afterEach, as the Testing Library documentation describes.
  • Vitest’s default reporter prints console output grouped by timing, not by test, and the grouping changed between runs. The course’s reporter.mjs collects each log with its test and prints them in order at the end, and check.sh runs one test file per Vitest run.
  • React’s development warnings, the missing key or the controlled input, go to console.error, and the reporter prints them with a console.error: prefix, so the lessons quote them from the tests.
  • A counter at module level, in a first version of lesson 3’s StrictMode test, kept counting from one test to the next: Vitest isolates test files, not tests. The impure component now receives the array it changes as a prop.
  • tsconfig.app.json first had the lib DOM.Iterable, to spread a NodeList in a test. With TypeScript 7.0.2, DOM alone accepts it; the course’s configuration now has the template’s ["ES2023", "DOM"].

2026-09-15 — @types/react and React 19.3

Section titled “2026-09-15 — @types/react and React 19.3”
  • @types/react 19.3.0 marks FormEvent and FormEventHandler as @deprecated, with the comment “FormEvent doesn’t actually exist”, and points to ChangeEvent, InputEvent and SubmitEvent. ChangeEvent has two type parameters, the current target and the target. Lesson 4 uses SubmitEvent<HTMLFormElement> for onSubmit.
  • A component’s return type, in tsc’s message, is Promise<ReactNode> | ReactNode: the types accept async components, for Server Components.
  • In StrictMode, React 19.3 renders an impure component twice on mount, and the DOM shows the second render’s output, “Played so far: G G”.
  • react-vite-examples.yml runs npm ci and bash check.sh on Ubuntu, Windows and macOS with Node.js 24.21.0. check.sh takes about 50 seconds on my Windows machine.
  • The first push was refused: the GitHub token used for pushing lacks the workflow scope, which GitHub requires to create a file under .github/workflows. The code was pushed without the workflow, and the three-OS run is still to do (to verify: cross-OS differences, such as the order of create-vite’s file list or oxlint’s output format).
  • Update 2026-09-22: the workflow reached main afterwards. Three runs on 2026-09-16 failed, and run 35098302366, at cd98475 (“lint with oxlint –format agent”), passed on Ubuntu, Windows and macOS. What the three failures were is not recorded here.

2026-09-15 — What the course found in GuitarAlchemist/ga

Section titled “2026-09-15 — What the course found in GuitarAlchemist/ga”

At commit 8cc8c5a, in ReactComponents/ga-react-components and Apps/ga-client; Apps/ga-dashboard is an Angular 21 application, outside this course. Nothing here has been reported to GA.

  • Expanded rows remembered by index. DynamicPanel.tsx keeps its expanded rows in a Set<number> of positions in the filtered, polled data. After a filter or a poll that changes the order, another row is expanded. Lesson 3 reproduces it in ExpandableList.tsx. The panel’s data is unknown[]; which field could serve as an identity depends on the panel definitions (to verify).
  • A render loop between two components. NotesSelector.tsx calls onNotesChange from an effect that depends on it, and ScaleSelector.tsx passes a new handler at each render, which stores a new array. The reduction in lesson 4 ends with “Maximum update depth exceeded”. ScaleSelector also stores scale, derived from the notes, in state. It is exported from components/index.ts, and no GA application renders it at this commit, so the loop is latent. I haven’t mounted GA’s component itself (to verify).
  • A sliding window keyed by index. DemerzelCriticOverlay.tsx draws the last ten scores with key={i}, and its bars have transition: height 0.3s ease: once the window is full, each new score changes and animates every bar. Lesson 2’s exercise 3 shows the element reuse; I haven’t watched the animation (to verify).
  • oxlint’s React rules. oxlint 1.83.0 with react/no-array-index-key, react/exhaustive-deps and typescript/no-explicit-any reports, in ga-react-components/src, 85 index keys, 34 effect or callback dependency lists that don’t match what the function reads, and 11 explicit any; in ga-client/src, 6 index keys. react/jsx-key and react/rules-of-hooks report nothing. The counts are lint results, not bugs: most index keys are on lists that never change.
  • Effects without cleanup. A rough scan of the useEffect calls that start a timer, a listener or a subscription and return no cleanup found one: ChatWidget.tsx starts a 300 ms setTimeout that isn’t cleared if the component unmounts first. The scan is a text search, not a parser (to verify before drawing conclusions).
  • Scene options. SceneOptions.tsx also notifies its parent from an effect, once at mount, with the dependency rule turned off. Its merge of URL parameters and saved options is already the subject of the open pull request GuitarAlchemist/ga#683, from the JavaScript course; this course doesn’t fix it again.
  • Build and dev server. ga-react-components’ build script is vite build without tsc, and its vite.config.ts, 3,005 lines, adds middleware for dozens of /dev-data/ endpoints behind a Cloudflare tunnel (lesson 1). Apps/ga-client’s dev script refuses to start, and says to use ga-react-components instead. The repository also tracks generated files: tsconfig.app.tsbuildinfo, playwright-report and test-results in ga-react-components.
  • #733, merged 2026-09-24, fixes both findings, each with a test that fails on the old code.
  • DynamicPanel remembers an expanded row by its identity: the row’s id, else its key, else its title field, with a suffix for repeats; only a row with none of these falls back to its position. DynamicPanel.test.tsx expands Beta, then filters with the chips or sends a poll with the rows reversed. On the old code both tests fail, showing Alpha’s detail instead of Beta’s.
  • The render loop is reproduced in GA’s own component, not only in the course’s reduction. The test mounts the real ScaleSelector, with its two diagram components mocked out, and makes the parent’s handler throw after 20 calls. On the old code it throws on mount and again after typing “C E G”; without that limit the test hung for more than 120 s. Both components now report from an effect that depends only on the notes, and keep the latest handler in a ref. With the fix, mounting makes at most 2 calls and typing makes exactly 1, with ['C','E','G'].
  • The loop never showed in the app: no page renders this ScaleSelector, and ga-client has its own.
  • The server.close() hang on Linux and macOS, and whether Vite has an issue for it.
  • GA’s ScaleSelector and NotesSelector mounted with Material UI, and the DemerzelCriticOverlay animation.
  • An identity field for DynamicPanel’s rows in GA’s panel definitions.