Skip to content

feature: migrate Conduit app from Angular to React + Vite#39

Open
devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1781621468-migrate-to-react
Open

feature: migrate Conduit app from Angular to React + Vite#39
devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1781621468-migrate-to-react

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 16, 2026

Copy link
Copy Markdown

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, the jwtToken localStorage key, the auth state machine, and the window.__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

  • Removed all Angular sources/configs (src/app/**, angular.json, tsconfig.spec.json, .browserslistrc, Angular *.spec.ts).
  • Added a React app under src/ (api/, context/, components/, pages/, services/, types/, utils/), root index.html, main.tsx, App.tsx, and Vite/Vitest configs. Build output path is unchanged (dist/angular-conduit/browser) so deploy.yml keeps working.
  • The realworld submodule (global CSS + media assets) is reused as-is; assets are copied via vite-plugin-static-copy.

Notable parity details (hard to infer from the diff alone)

Auth state machine (AuthContext) mirrors the Angular UserService: loading → authenticated | unauthenticated | unavailable, where a 4XX on GET /user logs out and a 5XX/network error enters unavailable with exponential-backoff retry. Routes only render once authState !== 'loading'.

HTTP interceptor parity (apiClient): injects Authorization: Token <jwt>; on a 401 for any endpoint other than /user it triggers a logout handler (the Angular errorInterceptor "token expired mid-session" behavior). Errors are normalized to { errors, status } so forms can render .error-messages and auth logic can branch on status.

Entry-time route guardRequireAuth guards only at mount, not reactively:

const wasAuthenticated = useRef(isAuthenticated);
if (isAuthenticated) wasAuthenticated.current = true;
return isAuthenticated || wasAuthenticated.current ? children : <Navigate to="/login" replace />;

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 /login before the error renders — which is exactly what several error-handling.spec.ts cases assert.

Defensive list rendering (ArticleList): the feed query tolerates malformed/empty API responses (treats a non-array articles as []) so a junk 200/204 body can't crash the home page — covered by the "malformed JSON" / "empty response body" edge-case tests.

Settings update: omits an empty password from the PUT /user payload (the backend returns 422 for password: ""), so updating only bio/image/email succeeds.

Verification

  • bun run build — production build OK
  • bun run format:check — clean
  • bun run test:e2e — 122 passed
  • bun run test:e2e:security — 16 passed

React Conduit home

Link to Devin session: https://app.devin.ai/sessions/c28706bd9e0646aeab57de75980044be
Requested by: @dillonvargo


Devin Review

Status Commit
⚪ Not started

Run Devin Review

Open in Devin Review (Staging)

devin-ai-integration Bot and others added 2 commits June 16, 2026 17:16
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-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

…between login/register

Co-Authored-By: Dillon Vargo <dillonvargo@gmail.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment thread src/pages/Article.tsx Outdated
return () => {
cancelled = true;
};
}, [article]);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
}, [article]);
}, [article?.body]);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

devin-ai-integration Bot and others added 2 commits June 26, 2026 19:48
…te/follow toggle

Co-Authored-By: Dillon Vargo <dillonvargo@gmail.com>
Co-Authored-By: Dillon Vargo <dillonvargo@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants