Skip to content

Migrate Conduit (Angular) to React 18 + Vite in react-app/#35

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

Migrate Conduit (Angular) to React 18 + Vite in react-app/#35
devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1781118987-angular-to-react-migration

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown

Summary

Adds a 1:1 port of the Conduit app from Angular to React 18 + TypeScript + Vite in a new react-app/ directory, without removing the Angular source (per the migration playbook's "Do not remove Angular components" rule). Every route, the external RealWorld API contract, and the custom Conduit theme are preserved. Build, lint, and unit tests are green; the Playwright e2e suite now runs against the React app with full parity to the Angular baseline.

The migration follows a direct mechanical mapping: Angular services → a single apiClient fetch wrapper + plain async API modules; the auth interceptor/UserService → React Context (AuthProvider); RxJS Observables → async/await + AbortController; Angular pipes → utility functions; templates → JSX.

Architecture mapping (Angular → React)

Angular React (react-app/src)
3 HTTP interceptors (api url, token, error/401 purge) api/apiClient.ts (single fetch wrapper)
UserService / JwtService (identity, session) context/AuthProvider.tsx + hooks/useAuth.ts + api/jwt.ts
ArticlesService, CommentsService, ProfileService, TagsService, UserService.update api/{articles,comments,profiles,tags,users}.ts (async fns)
requireAuth / requireUnauth route guards components/RouteGuards.tsx (RequireAuth / RedirectIfAuthenticated)
DatePipe, MarkdownPipe, DefaultImagePipe utils/{date,markdown,defaultImage}.ts
Standalone components + templates components/*.tsx, pages/*.tsx (JSX)
app.config.ts routing App.tsx (React Router v6)

Routes preserved (exact URL structure)

/, /tag/:tag (via query/state), /login, /register, /settings, /editor, /editor/:slug, /article/:slug, /profile/:username, /profile/:username/favorites.

Behavioral parity notes (high-entropy)

  • API contract unchanged: same base URL https://api.realworld.show/api, JWT in localStorage['jwtToken'], sent as Authorization: Token <jwt>. Article create still POSTs to the canonical /articles/ path (matches Angular + e2e mocks).
  • Route-guard latching: Angular's canActivate decides once per navigation and latches. RequireAuth replicates this with a useRef so a mid-session global 401 (e.g. a failed write) does not yank the user off a protected page — the form stays open to show the error, matching Angular UX. Eject only happens via explicit logout().
  • Theme: the custom "Conduit Minimal CSS" theme and all classnames are preserved (no Tailwind was actually used by the Angular app despite the issue text; styling is the shared realworld theme). Theme media assets are copied to react-app/public/assets/ so Vite can serve them.
  • PWA: neither the Angular app nor this port is a PWA — nothing to migrate.

CI / tooling

  • New .github/workflows/react.yml: bun installlinttest (Vitest) → build for react-app/ (deterministic, green).
  • playwright.config.ts now boots react-app (bun run dev, cwd react-app) on port 4200; playwright.yml + security-tests.yml install react-app deps before running.
  • Root bun run format:check (the existing "Format Check" job) passes over the new files.

Verification

  • cd react-app && bun run build → zero errors
  • cd react-app && bun run lint → zero warnings
  • cd react-app && bun run test → 8 passing unit tests (utils + ListErrors)
  • bun run test:e2e (against React) → 116 passed, 6 failed; @security → 16/16 passed. The 6 failures are pre-existing settings.spec.ts cases that hit the live API and fail identically on the Angular baseline (the live demo backend rejects the empty password field both apps send) — not a regression.

Manually verified in the browser (screenshots below): global feed + popular tags, article view with rendered markdown + comments, register → authenticated state, publish article via editor, post comment, profile page.

Not done (needs a decision)

The issue asks to remove the Angular source after verification, but the migration playbook explicitly forbids removing Angular components. I left Angular in place and documented the React app in the README instead. Removal can be a fast follow-up PR once confirmed.

Screenshots

Home (global feed + popular tags):
home

Article view (markdown rendered, comments):
article

Authenticated home after register (New Article / Settings / username, Your Feed tab):
authed

Profile page:
profile

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


Devin Review

Status Commit
🟢 Reviewed 0f05267
Open in Devin Review (Staging)

devin-ai-integration Bot and others added 3 commits June 10, 2026 19:51
… wire CI

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

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

Copy link
Copy Markdown
Author

CI status

Check Result
build-lint-test (React: build + ESLint + Vitest) ✅ pass
security-tests (Playwright @security) ✅ pass (16/16)
format (root Prettier) ✅ pass
test (Playwright e2e) ⚠️ 114 passed, 2 flaky→passed, 6 failed

The 6 test failures are pre-existing on the Angular baseline — not a regression

All 6 failures are in e2e/settings.spec.ts (lines 18, 58, 95, 134, 155, 197). They hit the live RealWorld API (api.realworld.show) and fail because the live demo backend rejects the profile update both apps send — the request never persists, so the page stays on /settings instead of redirecting to the profile.

Evidence this is pre-existing and unrelated to the React migration: the unrelated Angular-only PR [#?] run 27281651440 (branch devin/1781099978-s2699-test-assertions, no app-code changes) fails the exact same settings.spec.ts:18/58/95/134/... cases against the Angular app. The Playwright test job is therefore red on main as well.

Per the migration's 1:1 mandate I did not modify these tests or mock the live API to force them green, since that would diverge from the Angular baseline behavior.

@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 3 potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment thread react-app/src/App.tsx
Comment on lines +132 to +139
const updateUser = useCallback(
async (user: UserSettingsUpdate) => {
const updated = await updateUserRequest(user);
updateCurrentUser(updated);
return updated;
},
[updateCurrentUser],
);

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.

🚩 updateUser does not persist the returned JWT token to localStorage

The updateUser callback at react-app/src/context/AuthContext.tsx:132-139 calls updateCurrentUser(updated) but does not call saveToken(updated.token). The PUT /user API returns a user object that includes a token field. If the backend rotates the JWT on user update, the in-memory user will have the new token but localStorage will retain the stale one. On next page refresh, the app would attempt to authenticate with the old token.

However, this is consistent with the Angular version at src/app/core/auth/services/user.service.ts:158-164, which also only calls currentUserSubject.next(user) without saving the token. So this is a faithful port of a pre-existing behavior — not a regression. Worth fixing in both versions if the demo API ever starts rotating tokens on user update.

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.

Intentionally left as-is. As noted, this faithfully mirrors the Angular UserService.update (src/app/core/auth/services/user.service.ts), which only pushes the user into the in-memory subject and does not re-persist the token. Since this PR is a strict 1:1 migration, changing the token-persistence behavior here would diverge from the baseline. The live demo API does not rotate the JWT on PUT /user, so there's no observable impact today; if that ever changes it should be fixed in both apps together.

Comment thread playwright.config.ts
Comment on lines +17 to +18
command: 'bun run dev',
cwd: 'react-app',

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.

🚩 Playwright E2E tests now target the React app instead of Angular

The playwright.config.ts change switches the web server from npm run start (Angular CLI dev server) to bun run dev with cwd: 'react-app' (Vite dev server). This means all E2E tests in the e2e/ directory now run exclusively against the React app. Any behavioral differences between the Angular and React implementations could cause E2E test regressions. The Angular app's dev server is no longer exercised by CI at all. This is presumably intentional as part of the migration, but worth confirming that the Angular app isn't meant to continue being tested independently.

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.

Confirmed intentional. The goal of this PR is to validate the React port against the existing e2e suite, so the Playwright webServer now boots the React app. The Angular build/unit tests still run via the existing test job (root bun run test) and the Angular source is untouched. If you'd prefer to keep an Angular e2e job running in parallel during the transition, that's easy to add — let me know and I'll wire up a second webServer/project. Final disposition of the Angular source (keep vs. remove) is the open question on the PR.

@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 2 potential issues.

Open in Devin Review (Staging)
Debug

Playground

Comment on lines +132 to +138
const updateUser = useCallback(
async (user: UserSettingsUpdate) => {
const updated = await updateUserRequest(user);
updateCurrentUser(updated);
return updated;
},
[updateCurrentUser],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 updateUser does not persist the new token from the API response to localStorage

The updateUser callback in AuthContext calls updateCurrentUser(updated) to update the in-memory user state, but unlike setAuth (used by login/register at react-app/src/context/AuthContext.tsx:50-58), it never calls saveToken(updated.token). The PUT /user RealWorld API endpoint returns a full user object including a token field. If the backend returns a refreshed/rotated token, currentUser.token (in-memory) and localStorage.jwtToken (used by apiClient at react-app/src/api/apiClient.ts:63 for all subsequent requests) will diverge. Subsequent API calls will use the stale localStorage token, potentially causing silent 401 failures.

The Angular version (src/app/core/auth/services/user.service.ts:158-163) has the same omission, so existing E2E tests pass, but the inconsistency between setAuth and updateUser is a latent correctness issue.

Suggested change
const updateUser = useCallback(
async (user: UserSettingsUpdate) => {
const updated = await updateUserRequest(user);
updateCurrentUser(updated);
return updated;
},
[updateCurrentUser],
const updateUser = useCallback(
async (user: UserSettingsUpdate) => {
const updated = await updateUserRequest(user);
saveToken(updated.token);
updateCurrentUser(updated);
return updated;
},
[updateCurrentUser],
);
Open in Devin Review (Staging)

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

Debug

Playground

Comment thread playwright.config.ts
Comment on lines +17 to +18
command: 'bun run dev',
cwd: 'react-app',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Playwright config now runs React app instead of Angular — Angular E2E coverage removed

The root playwright.config.ts was changed from command: 'npm run start' (Angular) to command: 'bun run dev', cwd: 'react-app' (React). This means all existing E2E tests in e2e/ now run exclusively against the React app. The Angular app's E2E coverage is silently dropped. This is presumably intentional for the migration, but it means the Angular app (still present in src/) no longer has automated E2E validation. If both apps need to be maintained in parallel, a separate Playwright config for Angular would be needed.

Open in Devin Review (Staging)

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

Debug

Playground

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

View 8 additional findings in Devin Review.

Open in Devin Review

event.preventDefault();
setIsSubmitting(true);
try {
const user = await updateUser({ image, username, bio, email, 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.

🔴 Settings page always sends empty password string to API, potentially resetting user's password

In SettingsPage.tsx, password state is initialized to '' (line 18) and is always included in the updateUser call (line 35): await updateUser({ image, username, bio, email, password }). When the user updates any setting without typing a new password, the request payload includes password: ''. Depending on the backend implementation, this could overwrite the user's password with an empty string, locking them out.

The Angular version (src/app/features/settings/settings.component.ts:31-34) had Validators.required on the password FormControl, preventing form submission when the password was empty. The React port drops this validation entirely, meaning every profile update sends password: '' to PUT /user.

Fix: only include password when non-empty

Filter out the empty password before sending to the API:

const payload = { image, username, bio, email, ...(password ? { password } : {}) };
const user = await updateUser(payload);
Suggested change
const user = await updateUser({ image, username, bio, email, password });
const user = await updateUser({ image, username, bio, email, ...(password ? { password } : {}) });
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.

I dug into this and intentionally kept the React port identical to Angular — the premise that Angular prevented submission isn't accurate.

Angular's password control does have Validators.required, but the submit button is <button type="submit">Update Settings</button> with no [disabled]="settingsForm.invalid" binding (src/app/features/settings/settings.component.html — the only disable is <fieldset [disabled]="isSubmitting()">). In Angular reactive forms, (ngSubmit) fires regardless of form validity, and submitForm() sends this.settingsForm.value, which includes password: ''. So the Angular app already sends { user: { ..., password: '' } } to PUT /user on every save without a new password — exactly what this React port does.

This is in fact the root cause of the 6 pre-existing settings.spec.ts e2e failures, which fail identically on the Angular baseline (the live api.realworld.show backend now rejects the empty-password payload). Applying the suggested ...(password ? { password } : {}) filter would make the React app diverge from Angular (its settings e2e tests would start passing while Angular's still fail), which breaks the 1:1 parity this migration is mandated to preserve.

So I'm leaving it as a faithful port. It's a real latent issue worth fixing — but it should be fixed in both apps in a separate change, not silently introduced in the React port. I've flagged this to the requester.

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

Open in Devin Review (Staging)
Debug

Playground

Comment on lines +132 to +139
const updateUser = useCallback(
async (user: UserSettingsUpdate) => {
const updated = await updateUserRequest(user);
updateCurrentUser(updated);
return updated;
},
[updateCurrentUser],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Angular update() also omits saveToken — the React port faithfully mirrors this

The bug I reported (BUG-0001) in updateUser not saving the token is actually a pre-existing pattern from the Angular UserService.update() at src/app/core/auth/services/user.service.ts:158-163, which also only calls this.currentUserSubject.next(user) without this.jwtService.saveToken(user.token). The React port faithfully replicates this. Whether to fix it depends on whether the demo backend at api.realworld.show ever rotates the JWT on PUT /user. For most Conduit implementations the token stays stable, so the practical impact is low.

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