Replace React/Inertia with Blade + Livewire 4 #52

Closed
opened 2026-08-15 13:35:35 +02:00 by myrmidex · 3 comments
Owner

The stack swap. Deliberately sequenced last: after #47–#51 the frontend is one display component plus a dialog, so this is a rewrite of ~2 components rather than a port of a codebase.

Rationale

For a page that is one number and a dialog, the React starter kit's baseline (layout system, component library, SSR, a TS type mirroring every model) is pure overhead. wire:click="increment" is the entire feature spec as one line of markup.

Mirror ffr's setup — same Livewire 4 major, same Tailwind 4 + Vite toolchain — so patterns lift directly between the two projects.

Scope

Add:

  • livewire/livewire — pin to the same major ffr uses (^4.0)
  • app/Livewire/Counter.phpincrement(), setCount(int), #[Validate] on the dialog input
  • resources/views/livewire/counter.blade.php
  • Root layout Blade view

Remove:

  • inertiajs/inertia-laravel, tightenco/ziggy (composer)
  • @inertiajs/react, react, react-dom, @types/react, @types/react-dom (npm)
  • app/Http/Middleware/HandleInertiaRequests.php and HandleAppearance.php, plus both registrations in bootstrap/app.php:20-21
  • resources/js/app.tsx, resources/js/ssr.tsx, build:ssr script, ssr entry in vite.config.ts
  • resources/js/types/, resources/js/hooks/, remaining components/
  • eslint.config.js, tsconfig.json, components.json, .prettierrc if nothing else uses them

Port, don't rewrite: the black/red monospace terminal aesthetic is the asset here. LedDisplay.tsx is only 67 lines but its Tailwind classes carry the whole look — copy the markup and classes into Blade rather than restyling from scratch.

Acceptance criteria

  • Clicking the number increments it and re-renders without a full page load
  • The dialog sets an absolute value
  • Visual output matches the current terminal aesthetic
  • package.json dependencies reduced to roughly Tailwind + Vite, as in ffr
  • No Inertia or React references remain anywhere
  • Full suite green; lint + PHPStan clean
The stack swap. Deliberately sequenced last: after #47–#51 the frontend is one display component plus a dialog, so this is a rewrite of ~2 components rather than a port of a codebase. ## Rationale For a page that is one number and a dialog, the React starter kit's baseline (layout system, component library, SSR, a TS type mirroring every model) is pure overhead. `wire:click="increment"` is the entire feature spec as one line of markup. Mirror ffr's setup — same Livewire 4 major, same Tailwind 4 + Vite toolchain — so patterns lift directly between the two projects. ## Scope **Add:** - `livewire/livewire` — pin to the same major ffr uses (`^4.0`) - `app/Livewire/Counter.php` — `increment()`, `setCount(int)`, `#[Validate]` on the dialog input - `resources/views/livewire/counter.blade.php` - Root layout Blade view **Remove:** - `inertiajs/inertia-laravel`, `tightenco/ziggy` (composer) - `@inertiajs/react`, `react`, `react-dom`, `@types/react`, `@types/react-dom` (npm) - `app/Http/Middleware/HandleInertiaRequests.php` and `HandleAppearance.php`, plus both registrations in `bootstrap/app.php:20-21` - `resources/js/app.tsx`, `resources/js/ssr.tsx`, `build:ssr` script, `ssr` entry in `vite.config.ts` - `resources/js/types/`, `resources/js/hooks/`, remaining `components/` - `eslint.config.js`, `tsconfig.json`, `components.json`, `.prettierrc` if nothing else uses them **Port, don't rewrite:** the black/red monospace terminal aesthetic is the asset here. `LedDisplay.tsx` is only 67 lines but its Tailwind classes carry the whole look — copy the markup and classes into Blade rather than restyling from scratch. ## Acceptance criteria - [ ] Clicking the number increments it and re-renders without a full page load - [ ] The dialog sets an absolute value - [ ] Visual output matches the current terminal aesthetic - [ ] `package.json` dependencies reduced to roughly Tailwind + Vite, as in ffr - [ ] No Inertia or React references remain anywhere - [ ] Full suite green; lint + PHPStan clean
myrmidex added this to the (deleted) milestone 2026-08-15 13:35:35 +02:00
myrmidex added the
enhancement
label 2026-08-15 13:35:35 +02:00
myrmidex self-assigned this 2026-08-15 13:35:35 +02:00
myrmidex modified the milestone from (deleted) to v0.4.0 2026-08-15 13:42:26 +02:00
Author
Owner

Additional scope: production Dockerfile will break

docker/production/Dockerfile copies three files this ticket deletes:

16:COPY tsconfig.json ./
17:COPY components.json ./
18:COPY eslint.config.js ./

Removing them without updating the Dockerfile breaks the production image build — and it would fail at release time, not in CI, since .forgejo/workflows/build.yml only runs on main pushes and v* tags.

The frontend-builder stage will also need revisiting: it exists to run the Vite/React build, which after the Livewire migration is only Tailwind. Worth checking whether the multi-stage split still earns its keep.

Add to acceptance criteria:

  • docker/production/Dockerfile builds successfully after the deletions
  • Multi-arch (linux/amd64,linux/arm64) production build still works
## Additional scope: production Dockerfile will break `docker/production/Dockerfile` copies three files this ticket deletes: ``` 16:COPY tsconfig.json ./ 17:COPY components.json ./ 18:COPY eslint.config.js ./ ``` Removing them without updating the Dockerfile breaks the production image build — and it would fail at release time, not in CI, since `.forgejo/workflows/build.yml` only runs on `main` pushes and `v*` tags. The frontend-builder stage will also need revisiting: it exists to run the Vite/React build, which after the Livewire migration is only Tailwind. Worth checking whether the multi-stage split still earns its keep. Add to acceptance criteria: - [ ] `docker/production/Dockerfile` builds successfully after the deletions - [ ] Multi-arch (`linux/amd64,linux/arm64`) production build still works
Author
Owner

PHPStan level 8 finding in a file this ticket deletes

From the #55 setup — trying level 8 surfaced two errors in HandleInertiaRequests.php, which this ticket removes along with Inertia:

app/Http/Middleware/HandleInertiaRequests.php:45
  Parameter #1 $string of function trim expects string, string|null given.  (x2)

No action needed beyond the deletion already scoped here.

Once this and #53 land, level 8 is within reach — the only remaining errors are five Model::first() nullable-return cases in tests, each a one-line fix. Noted on #53.

## PHPStan level 8 finding in a file this ticket deletes From the #55 setup — trying level 8 surfaced two errors in `HandleInertiaRequests.php`, which this ticket removes along with Inertia: ``` app/Http/Middleware/HandleInertiaRequests.php:45 Parameter #1 $string of function trim expects string, string|null given. (x2) ``` No action needed beyond the deletion already scoped here. Once this and #53 land, level 8 is within reach — the only remaining errors are five `Model::first()` nullable-return cases in tests, each a one-line fix. Noted on #53.
Author
Owner

Done — 6ce4703

44 files, −7,456 lines net. React and Inertia are gone.

Before After
Build output 2264 modules, 311 kB JS 2 modules, 0 kB JS
CSS 34.5 kB 13.6 kB
npm dependencies 33 4
app/ PHP files 16 4
routes/web.php 8 routes 1 line

The whole application is now: app/Livewire/Counter.php, app/Models/Tracker.php, resources/views/livewire/counter.blade.php, resources/views/layouts/app.blade.php, and 24 lines of CSS.

Added

  • app/Livewire/Counter.phpmount(), initialise(), increment(), edit(), save(), cancel()
  • resources/views/livewire/counter.blade.php — onboarding and counter in one view
  • resources/views/layouts/app.blade.php — mirrors ffr's ->layout('layouts.app') convention

Removed

All of resources/js/ (19 files), app.blade.php, HandleInertiaRequests, HandleAppearance, config/inertia.php, eslint.config.js, tsconfig.json, components.json, .prettierrc, .prettierignore, the dev:ssr script, inertiajs/inertia-laravel, tightenco/ziggy, and both orphaned controllers.

The aesthetic was ported, not rewritten — Tailwind classes copied verbatim from LedDisplay.tsx and SetCountForm.tsx. Only @font-face, .font-digital and .glow-red survive in CSS; the rest was shadcn theming for components deleted earlier in this milestone.

Deliberate decision: the JSON API is gone

GET/POST/PATCH /tracker, POST /increment and PATCH /count were removed along with CounterController and TrackerController. All state changes go through Livewire. Nothing in-repo consumed those endpoints; anything external scripting them would break.

Security hardening

$count and $needsOnboarding are #[Locked]. Livewire public properties are client-writable, and tampering with needsOnboarding would have exposed the onboarding form on a configured app, with initialise() then attempting a second Tracker::create().

initialise() now adopts an existing tracker (Tracker::current() ?? Tracker::create(...)) rather than creating blindly, so two tabs onboarding concurrently cannot produce duplicate rows. There is no unique constraint on trackers to catch this at the DB level.

From review

  • increment() had no ceiling checksave() and initialise() validated against max:4294967295 but increment() did not, so at the unsigned-int ceiling the next click would overflow or throw an unhandled DB error. Extracted Counter::MAX_COUNT, applied to both the guard and the validation rule, with a test.
  • @livewireScripts added for symmetry with @livewireStyles (Livewire 4 auto-injects, but the asymmetry read as accidental).
  • README badge and tech stack updated to Livewire 4 + Blade.
  • Declined: renaming vite.config.ts.js. Cosmetic; the file is valid TS and Vite handles it natively.

Also fixed

docker/production/Dockerfile copied tsconfig.json, components.json and eslint.config.js — all deleted here. That would have failed at release time, not in CI, since the image build only runs on main pushes and v* tags.

composer.json name/description were still the laravel/react-starter-kit defaults.

Gates

PHPUnit 33 tests, 81 assertions · PHPStan 0 errors (level 7) · Pint PASS 39 files · npm run build PASS

Verified live: GET / returns 200 with >1005< server-rendered in the initial HTML and wire:click="increment" wired. Livewire's runtime serves from its hashed asset path.

.claude/PLATFORM.md rewritten — it still described a User-owned tracker and the React stack.

## Done — `6ce4703` 44 files, −7,456 lines net. React and Inertia are gone. | | Before | After | |---|---|---| | Build output | 2264 modules, 311 kB JS | **2 modules, 0 kB JS** | | CSS | 34.5 kB | 13.6 kB | | npm dependencies | 33 | 4 | | `app/` PHP files | 16 | **4** | | `routes/web.php` | 8 routes | 1 line | **The whole application is now:** `app/Livewire/Counter.php`, `app/Models/Tracker.php`, `resources/views/livewire/counter.blade.php`, `resources/views/layouts/app.blade.php`, and 24 lines of CSS. ### Added - `app/Livewire/Counter.php` — `mount()`, `initialise()`, `increment()`, `edit()`, `save()`, `cancel()` - `resources/views/livewire/counter.blade.php` — onboarding and counter in one view - `resources/views/layouts/app.blade.php` — mirrors ffr's `->layout('layouts.app')` convention ### Removed All of `resources/js/` (19 files), `app.blade.php`, `HandleInertiaRequests`, `HandleAppearance`, `config/inertia.php`, `eslint.config.js`, `tsconfig.json`, `components.json`, `.prettierrc`, `.prettierignore`, the `dev:ssr` script, `inertiajs/inertia-laravel`, `tightenco/ziggy`, and both orphaned controllers. The aesthetic was ported, not rewritten — Tailwind classes copied verbatim from `LedDisplay.tsx` and `SetCountForm.tsx`. Only `@font-face`, `.font-digital` and `.glow-red` survive in CSS; the rest was shadcn theming for components deleted earlier in this milestone. ### Deliberate decision: the JSON API is gone `GET/POST/PATCH /tracker`, `POST /increment` and `PATCH /count` were removed along with `CounterController` and `TrackerController`. All state changes go through Livewire. Nothing in-repo consumed those endpoints; anything external scripting them would break. ### Security hardening `$count` and `$needsOnboarding` are `#[Locked]`. Livewire public properties are client-writable, and tampering with `needsOnboarding` would have exposed the onboarding form on a configured app, with `initialise()` then attempting a second `Tracker::create()`. `initialise()` now adopts an existing tracker (`Tracker::current() ?? Tracker::create(...)`) rather than creating blindly, so two tabs onboarding concurrently cannot produce duplicate rows. There is no unique constraint on `trackers` to catch this at the DB level. ### From review - **`increment()` had no ceiling check** — `save()` and `initialise()` validated against `max:4294967295` but `increment()` did not, so at the unsigned-int ceiling the next click would overflow or throw an unhandled DB error. Extracted `Counter::MAX_COUNT`, applied to both the guard and the validation rule, with a test. - `@livewireScripts` added for symmetry with `@livewireStyles` (Livewire 4 auto-injects, but the asymmetry read as accidental). - README badge and tech stack updated to Livewire 4 + Blade. - **Declined:** renaming `vite.config.ts` → `.js`. Cosmetic; the file is valid TS and Vite handles it natively. ### Also fixed `docker/production/Dockerfile` copied `tsconfig.json`, `components.json` and `eslint.config.js` — all deleted here. That would have failed at **release** time, not in CI, since the image build only runs on `main` pushes and `v*` tags. `composer.json` name/description were still the `laravel/react-starter-kit` defaults. ### Gates PHPUnit **33 tests, 81 assertions** · PHPStan 0 errors (level 7) · Pint PASS 39 files · `npm run build` PASS Verified live: `GET /` returns 200 with `>1005<` server-rendered in the initial HTML and `wire:click="increment"` wired. Livewire's runtime serves from its hashed asset path. `.claude/PLATFORM.md` rewritten — it still described a `User`-owned tracker and the React stack.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/incr#52
No description provided.