Skip to content

feat: migrate Angular Conduit app to React TypeScript#13

Open
devin-ai-integration[bot] wants to merge 9 commits intomainfrom
devin/1776376638-migrate-to-react-ts
Open

feat: migrate Angular Conduit app to React TypeScript#13
devin-ai-integration[bot] wants to merge 9 commits intomainfrom
devin/1776376638-migrate-to-react-ts

Conversation

@devin-ai-integration
Copy link
Copy Markdown

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

Summary

Adds a complete React + TypeScript rewrite of the Angular RealWorld (Conduit) blogging app in a new react-app/ directory. The original Angular source is not modified. Built with Vite, React Router v6, and plain fetch-based API services.

Stack: React 19, TypeScript 6, Vite 8, React Router 6, Sass
Structure:

  • src/pages/ — Route-level components: Home, Auth (Login/Register), ArticlePage, Editor, Settings, ProfilePage
  • src/components/ — Shared UI: Header, Footer, ArticleList, ArticlePreview, FavoriteButton, FollowButton, TagList, ListErrors
  • src/services/ — API layer: generic api.ts client + domain services (articles, auth, comments, profile, tags)
  • src/models/ — TypeScript interfaces for Article, User, Profile, Comment
  • src/context/ — AuthContext for JWT-based auth state

Features ported: Global feed, your feed, tag filtering, pagination, article CRUD, comments, favorites, follows, user auth (login/register/settings), profile pages with article/favorite tabs.

Visual parity with the Angular source was verified via automated pixel-level screenshot comparison (home, login, register views all <2% mismatch).

Updates since last revision

Round 6 — Addressed two more issues identified by Devin Review:

  1. Settings form empty on page refresh (Settings.tsx) — All form fields were initialized via useState(user?.image || ''), etc. Since useState only uses the initial value on mount, if user is null at mount time (e.g. page refresh while AuthContext is still loading the JWT), all fields started as empty strings and never updated. Added a useEffect that syncs form state when user becomes available.
  2. ArticleList fetch race condition (ArticleList.tsx) — The useEffect that fetches articles had no cleanup, so rapid feed tab switches could cause an earlier response to resolve after a later one, overwriting articles with data for the wrong feed. Added a cancelled flag with cleanup function to ignore stale responses.

Round 5 — Addressed two more issues identified by Devin Review:

  1. Comment card CSS class conflict (ArticlePage.tsx) — App.css defines comment styles under the .comment selector (borders, footer background, mod-options layout), but Round 4 had set className="card" for e2e compliance. Neither class alone satisfied both contracts. Fixed by using dual classes: className="card comment".card satisfies the e2e selector .card:not(.comment-form) .card-block, while .comment picks up the custom CSS styles from App.css.
  2. ProfilePage pagination not reset on user navigation (ProfilePage.tsx)useEffect(() => { setCurrentPage(1); }, [isFavorites]) only reset pagination when switching between "My Articles" and "Favorited Articles" tabs. Navigating from /profile/user1 (page 3) to /profile/user2 kept currentPage at 3. Added username to the dependency array: [isFavorites, username].

Round 4 — Addressed four more issues identified by Devin Review:

  1. Feed tab links used href="" instead of correct URLs (Home.tsx) — E2E tests assert toHaveAttribute('href', '/?feed=following') and toHaveAttribute('href', '/'). Replaced <a href="" onClick={...}> elements with React Router <Link to="/?feed=following"> and <Link to="/"> components for correct href attributes and proper SPA navigation.
  2. Comment cards reverted to className="card" (ArticlePage.tsx) — Round 3 changed this to "comment" for App.css, but e2e tests pervasively use .card:not(.comment-form) .card-block selectors (e2e/helpers/comments.ts:9, e2e/comments.spec.ts:34). Reverted to "card" since the Conduit CDN stylesheet provides .card styling and e2e contract compliance takes precedence. (Subsequently refined in Round 5 — see item 19.)
  3. Empty feed message missing .empty-feed-message class (ArticleList.tsx) — Added className="article-preview empty-feed-message", conditional content ("Your feed is empty" with Global Feed link when config.type === 'feed'), matching the Angular implementation and e2e/url-navigation.spec.ts:93-99 expectations.
  4. Stale closure in comment handlers (ArticlePage.tsx)handleAddComment and handleDeleteComment captured comments from the render closure. Rapid submissions could lose intermediate state. Switched both to functional updater form: setComments(prev => [comment, ...prev]) and setComments(prev => prev.filter(...)).

Round 3 — Addressed two issues identified by Devin Review:

  1. Comment cards used wrong CSS class (ArticlePage.tsx)Changed to className="comment" (subsequently refined through Rounds 4 and 5 — see items 16 and 19 above).
  2. Unmemoized listConfig caused duplicate fetches (Home.tsx, ProfilePage.tsx)listConfig was recreated as a new object reference on every render. Since ArticleList's useEffect depends on config by reference, any parent re-render (e.g. tags loading) triggered a redundant article API call. Wrapped with useMemo in both files. Also fixed Rules of Hooks violation in ProfilePage.tsx by moving useMemo above early return statements.

Round 2 — Addressed seven e2e test contract issues identified by Devin Review:

  1. Missing name attributes on Auth form inputs (Auth.tsx) — Added name="username", name="email", name="password" to match selectors in e2e/helpers/auth.ts.
  2. Missing name attributes on Editor form inputs (Editor.tsx) — Added name="title", name="description", name="body" to match selectors in e2e/helpers/articles.ts.
  3. Missing name attributes on Settings form inputs (Settings.tsx) — Added name="image", name="username", name="bio", name="email", name="password" to match selectors in e2e/settings.spec.ts.
  4. Feed toggle used <button> instead of <a> (Home.tsx) — E2E tests use a:has-text("Global Feed") / a:has-text("Your Feed") Playwright selectors. Replaced with <a href="" onClick>. (Further improved in Round 4 to use <Link> components — see item 15.)
  5. Wrong default avatar URL (ArticlePreview.tsx, ArticlePage.tsx, ProfilePage.tsx) — Changed from 'https://api.realworld.io/images/smiley-cyrus.jpeg' to '/default-avatar.svg' per e2e/SELECTORS.md. Copied the SVG file from realworld/assets/media/ to react-app/public/.
  6. Unfavorite discarded API response (ArticleList.tsx) — The unfavorite path manually computed favoritesCount - 1 instead of using the server's response. Now uses await unfavoriteArticle(article.slug) directly, matching ArticlePage.tsx.
  7. Missing window.__conduit_debug__ interface (AuthContext.tsx) — E2E tests access window.__conduit_debug__ for getToken(), getAuthState(), and getCurrentUser(). Added the interface using useRef hooks to track auth state synchronously, exposed via useEffect.

Round 1 — Addressed five bugs identified by Devin Review:

  1. XSS sanitization (ArticlePage.tsx) — Article body was rendered via dangerouslySetInnerHTML without sanitization. Added dompurify + marked to sanitize and parse markdown: DOMPurify.sanitize(marked.parse(article.body) as string).
  2. Editor stale state (Editor.tsx) — Navigating from edit → new article retained the previous article's form data. Added an else branch in the useEffect to clear all fields when slug is undefined.
  3. Auth form persistence (App.tsx) — Switching between /login and /register kept the previous form's email/password/errors because React reused the same <Auth> component instance. Added key="login" and key="register" to force remount on route change.
  4. DELETE response crash (api.ts)request() unconditionally called response.json() on success, which throws SyntaxError on DELETE endpoints returning 204/empty body. Added a check for status === 204 or content-length: 0 to return undefined instead.
  5. Favorite toggle data staleness (ArticlePage.tsx)handleFavoriteToggle was computing the unfavorited count client-side instead of using the server response. Now uses await unfavoriteArticle() response for both paths and wraps in try-catch.

Review & Testing Checklist for Human

  • E2E tests not actually executed — All e2e contract changes (items 6–12, 15–17, 19) are based on reading e2e helper code and SELECTORS.md, but no e2e tests were run against the React app. Run npx playwright test from the repo root to verify selectors actually match.
  • API base URLsrc/services/api.ts hardcodes https://api.realworld.io/api. This endpoint was returning HTTP 530 (Cloudflare) during development. Verify it is active, or update the URL if the backend has moved.
  • Comment card dual-class approachclassName="card comment" satisfies both e2e selectors and App.css styles, but verify visually that .card and .comment CSS rules don't produce unintended conflicts (e.g. doubled borders, competing padding).
  • No route guards — Unauthenticated users can navigate directly to /settings and /editor and will see broken/empty forms. Verify this is acceptable or add redirects.
  • Run the app end-to-endcd react-app && npm install && npm run dev, then exercise: create account → write article (verify markdown renders safely) → delete article (verify no crash) → favorite/unfavorite → add/delete comment → follow user → update settings → refresh /settings (verify fields populate) → logout → switch between login/register (verify fields reset). Also test navigating between different user profiles to confirm pagination resets, and rapidly switch feed tabs to verify no stale data appears.

Notes

  • This is part of a batch migration of 5 Angular repos to React TS. Related PRs exist in angular2-hn, angular-jasmine-karma-demo, angular-testing-course, and angularjs-asp-net48-mvc5.
  • package-lock.json accounts for ~3,400 lines of the diff — the actual application code is roughly 1,800 lines across 30 source files.
  • TypeScript compiles cleanly with tsc --noEmit and npm run build succeeds.
  • No unit or integration tests are included. The Angular source has Vitest/Playwright tests.
  • CSS is fully global (App.css). This is fine given the react-app/ directory isolation, but styles will collide if both Angular and React apps ever run in the same page.

Link to Devin session: https://app.devin.ai/sessions/6bdc9c8817d84868be64c8949279638a
Requested by: @lburgers


Open with Devin

- Full React+TypeScript implementation with Vite build system
- Pages: Home, Auth (Login/Register), ArticlePage, Editor, Settings, ProfilePage
- Components: Header, Footer, ArticleList, ArticlePreview, FavoriteButton, FollowButton, TagList
- Services: API client, articles, auth, comments, profile, tags services
- Features: JWT auth, article CRUD, comments, favorites, follows, tag filtering, global/your feed
- CSS: Pixel-perfect visual parity with source Angular app (verified via automated screenshot comparison)
- TypeScript: Strict typing with proper interfaces for all models
- Visual regression tested: <5% mismatch across home, login, register views

Co-Authored-By: Lukas Burger <lukaskburger@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 and CI monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

- Add DOMPurify + marked for article body sanitization (XSS fix)
- Clear Editor form fields when navigating from edit to new article
- Add key props to Auth routes to reset state between login/register
- Addresses Devin Review findings

Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

- Handle 204/empty body responses in api.ts request() to prevent JSON parse errors on DELETE endpoints
- Use server response for unfavorite (was using stale client-side count)
- Add try-catch to handleFavoriteToggle for error handling
- Addresses Devin Review findings

Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…rface

- Add name attributes to Auth, Editor, Settings form inputs for e2e test compatibility
- Replace button elements with anchor elements in Home feed toggle (e2e uses a:has-text selectors)
- Change default avatar URL to /default-avatar.svg (matches e2e SELECTORS.md contract)
- Fix ArticleList unfavorite to use server response instead of stale client-side count
- Add window.__conduit_debug__ interface with getToken, getAuthState, getCurrentUser

Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
className="form-control form-control-lg"
type="password"
placeholder="Password"
name="password"
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 — added name="username", name="email", and name="password" attributes to all Auth.tsx form inputs, matching the e2e test contract in e2e/helpers/auth.ts.

<input className="form-control" type="text" placeholder="What's this article about?" name="description" value={description} onChange={(e) => setDescription(e.target.value)} />
</fieldset>
<fieldset className="form-group">
<textarea className="form-control" rows={8} placeholder="Write your article (in markdown)" name="body" value={body} onChange={(e) => setBody(e.target.value)} />
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 — added name="title", name="description", and name="body" attributes to all Editor.tsx form inputs, matching the e2e test contract in e2e/helpers/articles.ts.

<input className="form-control form-control-lg" type="text" placeholder="Email" name="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</fieldset>
<fieldset className="form-group">
<input className="form-control form-control-lg" type="password" placeholder="New Password" name="password" value={password} onChange={(e) => setPassword(e.target.value)} />
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 — added name="image", name="username", name="bio", name="email", and name="password" attributes to all Settings.tsx form inputs, matching the e2e test contract in e2e/settings.spec.ts.

},
getCurrentUser: () => userRef.current,
};
return () => { delete window.__conduit_debug__; };
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 — added window.__conduit_debug__ interface to AuthContext.tsx with getToken(), getAuthState(), and getCurrentUser() functions. Uses refs to read current auth state synchronously, matching the Angular implementation in src/app/app.config.ts. Returns 'loading', 'authenticated', or 'unauthenticated' states as required by the e2e tests.

Comment thread react-app/src/pages/Home.tsx Outdated
onClick={(e) => { e.preventDefault(); setSearchParams({}); navigate('/'); }}
>
Global Feed
</a>
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 — replaced <button> elements with <a> elements for both "Your Feed" and "Global Feed" toggles, using href="" with e.preventDefault() + onClick handlers. This matches the a:has-text() Playwright selectors used in e2e/navigation.spec.ts.

}

function ArticlePreview({ article, onFavoriteToggle }: Props) {
const defaultImage = '/default-avatar.svg';
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 — changed default avatar URL to /default-avatar.svg in ArticlePreview.tsx, ArticlePage.tsx, and ProfilePage.tsx. Also copied the SVG file from the Angular source (realworld/assets/media/default-avatar.svg) to react-app/public/default-avatar.svg. Matches the e2e contract in e2e/SELECTORS.md.

try {
const updated = article.favorited
? await unfavoriteArticle(article.slug)
: await favoriteArticle(article.slug);
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 — the unfavorite path now uses the server response directly (await unfavoriteArticle(article.slug)) instead of manually constructing the article with a decremented count. This matches the pattern already used in ArticlePage.tsx and ensures the displayed count reflects the authoritative server state.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 2 commits April 16, 2026 22:34
Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…osure

Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…ername change

Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…leList

Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Author

Functional Test Results — All 5 React+TS Migrations

Method: Manual browser testing with screen recording across all 5 React apps running locally via Vite dev servers.

Result: All 5 apps passed functional testing.

Test 2: Conduit Blog (this PR) — PASSED
Assertion Result
Home page shows banner, Global Feed tab, Popular Tags sidebar Passed
Sign in route (/login) shows email + password form Passed
Typed test data into login form fields Passed
Sign up route (/register) shows username + email + password Passed
Email field EMPTY after navigating from login (key prop fix) Passed
Password field EMPTY after navigating from login (key prop fix) Passed

Login form with test data:
Login filled

Register form — fields EMPTY (key prop remount verified):
Register empty

Test 1: HN Clone (PR realworld-apps#64) — PASSED
  • Feed loads 16+ stories from HN API with points and comments
  • Navigation /news/1 → /newest/1 updates feed
  • Settings panel opens with Theme, Font Size, Spacing controls
  • Theme switching Default ↔ Night works

HN Feed

Test 3: Jasmine/Karma Demo (PR realworld-apps#55) — PASSED
  • "Get Users" fetches 10 users from JSONPlaceholder API
  • Shop route shows 3 item cards (foo, mario, luigi) with prices
  • Component remount on route change verified

Users loaded

Test 4: Testing Course (PR realworld-apps#45) — PASSED
  • BEGINNERS/ADVANCED tab switching works
  • About page shows Welcome heading and course image
  • Round-trip COURSES → ABOUT → COURSES routing works

About page

Test 5: ASP.NET MVC5 (PR realworld-apps#86) — PASSED
  • "Hello, angular-app" heading, congratulations text, resource links, social icons all present

ASP.NET MVC5


Known limitations (not app issues):

  • RealWorld API returns HTTP 530 — Conduit articles/tags don't load
  • Testing Course backend not running — course cards don't populate

Devin session

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.

1 participant