Journal
Sections
Reading settings
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
Progress
Section titled “Progress”- 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.jsonand lock file -
check.sh:create-vite, the dev server, an HMR update,tsc,vite build, oxlint, every error snippet and every test, compared withexpected/ - 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 |
2026-09-15 — Versions
Section titled “2026-09-15 — Versions”npm viewgives React and React DOM 19.3.0, released on 9 September 2026, Vite 8.3.0,@vitejs/plugin-react6.1.1, Vitest 5.0.0, jsdom 30.0.1,@testing-library/react16.3.3,@testing-library/dom10.4.2,@testing-library/user-event14.6.7, oxlint 1.83.0 andcreate-vite9.2.1. The course pins them exactly incode/react-vite/package.json, and uses Node.js 24.21.0 and TypeScript 7.0.2, like the site’s other JavaScript courses.create-vite9.2.1’sreact-tstemplate asks for TypeScript~6.0.2, not 7.0: a range with~allows 6.0.x only. The course uses 7.0.2, andtsc -bchecks the template’s two projects without a change. Itsstrictis absent, because it has been the default since TypeScript 6.0, and its lint tool is oxlint rather than ESLint, with an--eslintoption 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-react6 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-*.
2026-09-15 — Capturing the dev server
Section titled “2026-09-15 — Capturing the dev server”- The dev server, the modules it serves and the HMR message are captured by scripts, not by hand:
dev-start.mjsstartsviteand stops it when the URL is printed,dev-module.mjsrequests one URL through Vite’s JavaScript API, andhmr.mjsconnects to the HMR WebSocket, edits a copy ofApp.tsxand prints the messages. - When the output isn’t a terminal, Vite prints no
press h + enter to show helpline. My firstdev-start.mjswaited for that line, never saw it, and left a server running on port 5199; it now waits foruse --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. Awaitingserver.environments.client.waitForRequestsIdle()beforeclose()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.shsetsMSYS_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 asindex-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 inApp.tsx. The heading changed, the counter and the capo kept their values, and a variable set onwindowbefore the edit survived, so the page hadn’t reloaded. For lesson 1, I also served the build with a type error withvite previewand clicked its button twice:Count is 01, thenCount is 011.
2026-09-15 — Vitest and Testing Library
Section titled “2026-09-15 — Vitest and Testing Library”- React Testing Library unmounts rendered components after each test only when the test framework exposes a global
afterEach. Vitest doesn’t, unlessglobals: trueis set, so the DOM of one test stayed in the next and queries found two forms.src/testing/setup.tscallscleanupinafterEach, 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.mjscollects each log with its test and prints them in order at the end, andcheck.shruns 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 aconsole.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.jsonfirst had the libDOM.Iterable, to spread aNodeListin a test. With TypeScript 7.0.2,DOMalone 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/react19.3.0 marksFormEventandFormEventHandleras@deprecated, with the comment “FormEvent doesn’t actually exist”, and points toChangeEvent,InputEventandSubmitEvent.ChangeEventhas two type parameters, the current target and the target. Lesson 4 usesSubmitEvent<HTMLFormElement>foronSubmit.- A component’s return type, in
tsc’s message, isPromise<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”.
2026-09-15 — The CI
Section titled “2026-09-15 — The CI”react-vite-examples.ymlrunsnpm ciandbash check.shon Ubuntu, Windows and macOS with Node.js 24.21.0.check.shtakes about 50 seconds on my Windows machine.- The first push was refused: the GitHub token used for pushing lacks the
workflowscope, 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 ofcreate-vite’s file list or oxlint’s output format). - Update 2026-09-22: the workflow reached
mainafterwards. Three runs on 2026-09-16 failed, and run 35098302366, atcd98475(“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.tsxkeeps its expanded rows in aSet<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 inExpandableList.tsx. The panel’s data isunknown[]; which field could serve as an identity depends on the panel definitions (to verify). - A render loop between two components.
NotesSelector.tsxcallsonNotesChangefrom an effect that depends on it, andScaleSelector.tsxpasses a new handler at each render, which stores a new array. The reduction in lesson 4 ends with “Maximum update depth exceeded”.ScaleSelectoralso storesscale, derived from the notes, in state. It is exported fromcomponents/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.tsxdraws the last ten scores withkey={i}, and its bars havetransition: 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-depsandtypescript/no-explicit-anyreports, inga-react-components/src, 85 index keys, 34 effect or callback dependency lists that don’t match what the function reads, and 11 explicitany; inga-client/src, 6 index keys.react/jsx-keyandreact/rules-of-hooksreport 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
useEffectcalls that start a timer, a listener or a subscription and return no cleanup found one:ChatWidget.tsxstarts a 300 mssetTimeoutthat 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.tsxalso 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’buildscript isvite buildwithouttsc, and itsvite.config.ts, 3,005 lines, adds middleware for dozens of/dev-data/endpoints behind a Cloudflare tunnel (lesson 1).Apps/ga-client’sdevscript refuses to start, and says to usega-react-componentsinstead. The repository also tracks generated files:tsconfig.app.tsbuildinfo,playwright-reportandtest-resultsinga-react-components.
2026-09-24 — Upstream fixes
Section titled “2026-09-24 — Upstream fixes”- #733, merged 2026-09-24, fixes both findings, each with a test that fails on the old code.
DynamicPanelremembers an expanded row by its identity: the row’sid, else itskey, else its title field, with a suffix for repeats; only a row with none of these falls back to its position.DynamicPanel.test.tsxexpands 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.
To verify
Section titled “To verify”- The
server.close()hang on Linux and macOS, and whether Vite has an issue for it. - GA’s
ScaleSelectorandNotesSelectormounted with Material UI, and the DemerzelCriticOverlay animation. - An identity field for
DynamicPanel’s rows in GA’s panel definitions.