feature: migrate Conduit app from Angular to React + Vite#39
feature: migrate Conduit app from Angular to React + Vite#39devin-ai-integration[bot] wants to merge 5 commits into
Conversation
Replace the Angular implementation with a React 18 + Vite + TypeScript SPA while preserving the RealWorld API contract, DOM/CSS classes, routes, the jwtToken localStorage key, the auth state machine (loading/authenticated/ unauthenticated/unavailable with 5xx retry) and the window.__conduit_debug__ interface. All framework-agnostic Playwright e2e tests (incl. @security) pass. Co-Authored-By: Dillon Vargo <dillonvargo@gmail.com>
Co-Authored-By: Dillon Vargo <dillonvargo@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…between login/register Co-Authored-By: Dillon Vargo <dillonvargo@gmail.com>
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [article]); |
There was a problem hiding this comment.
🟡 Article body markdown is re-parsed on every favorite or follow toggle, wasting CPU
The markdown rendering effect uses the entire article object as its dependency ([article] at src/pages/Article.tsx:56) instead of just the body text, so every user interaction that updates the article state (favoriting, unfavoriting, following, unfollowing) re-triggers the expensive markdown parse and HTML sanitization even though the body hasn't changed.
Impact: Users experience unnecessary CPU work and potential input lag on long articles whenever they interact with favorite or follow buttons.
Mechanism: effect fires on object-reference change even when body is identical
When onToggleFavorite (src/pages/Article.tsx:60-69) or toggleFollowing (src/pages/Article.tsx:72-75) runs, it calls setArticle(current => ({...current, ...})) which creates a new article object. Because the effect dependency at line 56 is [article] (an object reference comparison), the effect fires. It calls renderMarkdown(article.body) (src/utils/markdown.ts:8-10), which runs marked.parse() and DOMPurify.sanitize() on the same unchanged body string.
In the original Angular version, the MarkdownPipe was a pure pipe: <div [innerHTML]="a.body | markdown | async"></div>. Angular's pure pipe memoises based on input identity — since a.body is the same string reference across favorite/follow toggles (the spread preserves the original string), the pipe did NOT re-execute.
The fix is to change the dependency from [article] to [article?.body], so the effect only re-fires when the body text actually changes.
| }, [article]); | |
| }, [article?.body]); |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 55649cc. Changed the effect dependency from [article] to [article?.body], so the markdown parse + sanitize only re-runs when the body text actually changes — favorite/follow toggles (which spread a new article object but keep the same body string) no longer re-trigger it, matching Angular's pure-pipe memoization.
…te/follow toggle Co-Authored-By: Dillon Vargo <dillonvargo@gmail.com>
Co-Authored-By: Dillon Vargo <dillonvargo@gmail.com>
Summary
Replaces the Angular implementation of the RealWorld (Conduit) app with a React 18 + Vite + TypeScript SPA. The migration is behavior-preserving: it keeps the same RealWorld backend (
https://api.realworld.show/api), DOM structure / CSS classes, routes, thejwtTokenlocalStorage key, the auth state machine, and thewindow.__conduit_debug__interface that the framework-agnostic e2e suite depends on. All Playwright e2e tests pass locally: 122 functional + 16@security= 138 green, plus Prettier format check and the production build.The whole point of this repo's test contract (
e2e/SELECTORS.md+__conduit_debug__) is that it is framework-agnostic, so the bulk of the work was reproducing the exact runtime semantics the tests assert — not just the UI.What changed
src/app/**,angular.json,tsconfig.spec.json,.browserslistrc, Angular*.spec.ts).src/(api/,context/,components/,pages/,services/,types/,utils/), rootindex.html,main.tsx,App.tsx, and Vite/Vitest configs. Build output path is unchanged (dist/angular-conduit/browser) sodeploy.ymlkeeps working.realworldsubmodule (global CSS + media assets) is reused as-is; assets are copied viavite-plugin-static-copy.Notable parity details (hard to infer from the diff alone)
Auth state machine (
AuthContext) mirrors the AngularUserService:loading → authenticated | unauthenticated | unavailable, where a 4XX onGET /userlogs out and a 5XX/network error entersunavailablewith exponential-backoff retry.Routesonly render onceauthState !== 'loading'.HTTP interceptor parity (
apiClient): injectsAuthorization: Token <jwt>; on a 401 for any endpoint other than/userit triggers a logout handler (the AngularerrorInterceptor"token expired mid-session" behavior). Errors are normalized to{ errors, status }so forms can render.error-messagesand auth logic can branch on status.Entry-time route guard —
RequireAuthguards only at mount, not reactively:This matches Angular, where guards run on navigation rather than reactively. It matters because a mid-session 401 (e.g. submitting the editor/settings with an expired token) must surface the form error in place rather than yanking the user to
/loginbefore the error renders — which is exactly what severalerror-handling.spec.tscases assert.Defensive list rendering (
ArticleList): the feed query tolerates malformed/empty API responses (treats a non-arrayarticlesas[]) so a junk200/204body can't crash the home page — covered by the "malformed JSON" / "empty response body" edge-case tests.Settings update: omits an empty
passwordfrom thePUT /userpayload (the backend returns 422 forpassword: ""), so updating only bio/image/email succeeds.Verification
bun run build— production build OKbun run format:check— cleanbun run test:e2e— 122 passedbun run test:e2e:security— 16 passedLink to Devin session: https://app.devin.ai/sessions/c28706bd9e0646aeab57de75980044be
Requested by: @dillonvargo
Devin Review