Skip to content

feature: migrate Angular Conduit app to React + TypeScript + Vite#32

Open
devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
devin/1781027671-angular-to-react-migration
Open

feature: migrate Angular Conduit app to React + TypeScript + Vite#32
devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
devin/1781027671-angular-to-react-migration

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown

Summary

Full rewrite of the Angular RealWorld (Conduit) app to React 18 + TypeScript + Vite, following the 4-phase migration playbook. All routes, JWT auth, feature modules (articles, profile, settings, auth, feeds), styling, and the framework-agnostic Playwright e2e contract are preserved. The Angular source was removed only after the React app built and every page was verified running.

The external API (https://api.realworld.show/api) and all data contracts are unchanged. No new UI libraries were introduced (only react, react-router-dom, plus marked + dompurify for the markdown rendering the Angular app already did).

Angular → React pattern mapping

Angular React
HttpClient + RxJS Observables fetch + async/await, AbortController for cancellation
Services (DI) for state React Context (AuthContext) + hooks
Route guards / canActivate <RequireAuth> / <GuestRoute> wrappers
HTTP interceptors (api/token/error) single apiClient in src/services/api.ts
Pipes utility functions in src/utils/
ngOnInit / ngOnDestroy useEffect + cleanup
Lazy NgModules React.lazy + <Suspense>

Notable correctness details (non-obvious from the diff)

  • src/services/api.ts: a non-/user 401 is surfaced to the calling component (so the editor/comment forms can render the error) rather than force-redirecting. Invalid-token logout still happens at app init via GET /user (4XX → purge, 5XX/network → "unavailable" + exponential-backoff retry), matching the Angular errorInterceptor 4XX-vs-5XX logic.
  • src/services/articles.ts: create() POSTs to /articles/ (trailing slash) to match the Angular reference and the e2e contract's request-abort expectation.
  • src/pages/Article.tsx: the article fetch effect depends only on slug; canModify is computed (currentUser?.username === article.author.username) instead of stored, avoiding an abort/refetch churn when AuthContext refreshes the user object.
  • src/pages/Settings.tsx: password is only included in the update payload when non-empty (...(password ? { password } : {})) — empty-string passwords made the API return 422.
  • window.__conduit_debug__ is exposed from AuthContext so the shared e2e helpers can read auth state regardless of framework.

Migration phases

  1. Codebase analysis — cataloged every Angular model/service/component/route/style + the external API.
  2. Foundation — scaffolded Vite + React + TS, ported types, services, AuthContext, utils, and SCSS-derived styles.
  3. Components & pages — ported all shared components, layout (header/footer), and every route page; wired React Router preserving the exact URL structure (/, /login, /register, /settings, /editor, /editor/:slug, /article/:slug, /profile/:username, /profile/:username/favorites).
  4. Testing, CI/CD, cleanup — Vitest + React Testing Library unit tests, kept the Playwright e2e suite + GitHub Actions workflows (deploy artifact path updated to dist/), removed src/app/, angular.json, and Angular configs, and updated the README.

Verification

  • bun run build (tsc -b && vite build) — passes, zero TypeScript errors.
  • bun run format:check (Prettier) — passes.
  • bun run test (Vitest + RTL) — 12/12 pass.
  • bun run test:e2e (framework-agnostic Playwright suite, 122 tests) — passes against the shared live demo API; a few create/comment/session tests can be intermittently slow under rapid sequential load on the shared backend, which the suite's retries: 2 in CI absorbs.
  • Ran the Vite dev server (localhost:4200) and visually verified home feed, tags, navigation, login, and article/profile/settings pages render with live API data.

Running app

Home (Global Feed + Popular Tags):

Conduit home page running on React

Sign in page:

Conduit sign in page

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


Devin Review

Status Commit
🟢 Reviewed d09b48a
Open in Devin Review (Staging)

devin-ai-integration Bot and others added 3 commits June 9, 2026 18:41
Complete rewrite of the Angular RealWorld (Conduit) app to React 18 + Vite,
preserving all routes, JWT auth, feature modules (articles, profile, settings,
auth, feeds), and the framework-agnostic e2e contract.

- Angular services/RxJS -> React Context + fetch/async-await (AbortController)
- Angular guards/interceptors -> React Router guards + fetch api client
- Angular pipes -> utility functions; lazy NgModules -> React.lazy + Suspense
- Vitest + RTL unit tests; preserved Playwright e2e + CI workflows
- Removed Angular source after React app verified building/running

Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
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, CI, and merge conflict monitoring

@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 potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment thread src/services/api.ts
Comment on lines +66 to +79
if (!response.ok) {
let body: unknown = null;
try {
body = await response.json();
} catch {
body = null;
}

const normalized =
body && typeof body === 'object' && 'errors' in body
? (body as Errors)
: { errors: { network: ['Unable to connect. Please check your internet connection.'] } };

throw { ...normalized, status: response.status } as ApiError;

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.

🔴 Missing global 401 handling for non-/user endpoints causes broken auth state on token expiry

The Angular app's errorInterceptor (src/app/core/interceptors/error.interceptor.ts:40) called purgeAuth() on any 401 response from non-/user endpoints, handling the "token expired mid-session" scenario. The React api.ts has no equivalent — it simply throws the error to the caller. This means if a user's JWT expires during a session and they perform any action (favorite, comment, follow, etc.), they receive a 401 error but remain in the authenticated state with a stale token in localStorage. The UI continues showing them as logged in (header shows username/avatar, protected routes remain accessible) but every subsequent API call fails. Users must manually log out or refresh the page to recover.

Prompt for agents
The Angular error interceptor (src/app/core/interceptors/error.interceptor.ts) had global 401 handling that called purgeAuth() for all endpoints except GET /user. The React migration lost this behavior.

The challenge is that api.ts is a plain module with no access to the AuthContext. There are several approaches to restore this:

1. Add a callback/listener pattern: api.ts could accept a registered callback (e.g., onUnauthorized) that AuthContext sets up on mount. When api.ts sees a 401 on a non-/user endpoint, it calls this callback, which triggers purgeAuth().

2. Emit a custom event: api.ts could dispatch a custom DOM event like 'conduit:unauthorized'. AuthContext could listen for this event and call purgeAuth().

3. Use a shared module-level variable: Export a mutable reference from a shared module that AuthContext populates with its purgeAuth function.

The key constraint is that the /user endpoint must be excluded from this handling (it has its own 4XX vs 5XX logic in AuthContext.fetchCurrentUser). So the 401 handler in api.ts should skip paths ending in '/user'.

Relevant files: src/services/api.ts (add 401 detection), src/context/AuthContext.tsx (register the purgeAuth callback).
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.

Investigated this — it's a real behavioral difference from Angular, but restoring a global purgeAuth() on non-/user 401s breaks this repo's e2e contract, so I'm intentionally not applying it.

The repo's e2e/error-handling.spec.ts explicitly pins the intended mid-session 401 behavior to surface the error and keep the user on the page, not log out/redirect:

  • :99 401 on settings submit → expects .error-messages visible and the form still usable (no redirect).
  • :131 401 on posting a comment → expects the article page (nav.navbar, .article-content) to stay rendered (no redirect).
  • :63 401 on article creation (the /editor route is guarded) → expects .error-messages visible and input[name="title"] still visible.

I verified empirically: I added the exact suggested callback (api.ts calls a registered setUnauthorizedHandler on any non-/user 401, AuthContext registers purgeAuth), then ran the suite. error-handling.spec.ts:63 fails because purgeAuth() flips the guard and <RequireAuth> redirects /editor/login before the error can render:

1) e2e/error-handling.spec.ts:63 › should handle 400 on article creation
   Error: expect(locator('.error-messages')).toBeVisible() failed
   waiting for locator('.error-messages') — element(s) not found
   1 failed, 30 passed

(Reverted that experiment; the branch is back to the green state.)

The "invalid token" case that genuinely warrants logout is preserved — it's just handled at the auth boundary rather than globally: AuthContext.fetchCurrentUser calls purgeAuth() on a GET /user 4XX (and enters unavailable + backoff retry on 5XX/network), which is what e2e/user-fetch-errors.spec.ts asserts. So an expired token is detected and cleared on the next app load / auth refresh; mid-session action 401s render an inline error instead of yanking the user to /login, matching the contract above.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

Open in Devin Review (Staging)
Debug

Playground

Comment thread src/services/api.ts
Comment on lines +66 to +79
if (!response.ok) {
let body: unknown = null;
try {
body = await response.json();
} catch {
body = null;
}

const normalized =
body && typeof body === 'object' && 'errors' in body
? (body as Errors)
: { errors: { network: ['Unable to connect. Please check your internet connection.'] } };

throw { ...normalized, status: response.status } as ApiError;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Missing global 401 handler causes broken state when token expires mid-session

The Angular errorInterceptor (src/app/core/interceptors/error.interceptor.ts:40) globally handled 401 responses on all endpoints except /user by calling purgeAuth() — this was the safety net for "token expired mid-session" scenarios. The new React api.ts request function throws the error but never triggers a logout on 401. When a user's JWT expires while they're browsing, API calls (favorite, follow, comment, delete, etc.) silently fail with 401, the user stays visually "authenticated" in the UI, and they're stuck in a broken state where all actions fail. They must manually log out or hard-refresh to recover.

Angular error interceptor that was removed

The original interceptor at src/app/core/interceptors/error.interceptor.ts:40:

if (err.status === 401 && !req.url.endsWith('/user')) {
  userService.purgeAuth();
}

The new api.ts request() function at line 66-79 throws errors but has no equivalent 401→logout logic.

Prompt for agents
The api.ts request() function is missing the global 401 handler that the Angular errorInterceptor provided. In the Angular version, any 401 response on endpoints OTHER than /user would trigger purgeAuth() to log the user out (handling token-expired-mid-session scenarios).

The fix needs to replicate this in the React architecture. The challenge is that api.ts is a plain module without access to React context (AuthContext). Possible approaches:

1. Add a callback registration mechanism: api.ts exports a function like registerUnauthorizedHandler(callback). AuthContext calls this on mount with its purgeAuth function. The request() function calls this callback when it gets a 401 on a non-/user endpoint.

2. Use a module-level event emitter or pub/sub pattern: api.ts publishes a 401-unauthorized event. AuthContext subscribes and calls purgeAuth.

3. Import jwt.destroyToken directly in api.ts and call it on 401 (simpler but doesn't update React state).

Option 1 is closest to the original architecture. The key logic is: if response.status === 401 AND the path does not end with /user, call the registered unauthorized handler (which should be purgeAuth from AuthContext).
Open in Devin Review (Staging)

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

Debug

Playground

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