Skip to content

Migrate Angular 9 HN PWA to React 18 + Vite + TypeScript#357

Open
eashansinha wants to merge 13 commits into
masterfrom
devin/1781803206-react-migration-base
Open

Migrate Angular 9 HN PWA to React 18 + Vite + TypeScript#357
eashansinha wants to merge 13 commits into
masterfrom
devin/1781803206-react-migration-base

Conversation

@eashansinha

@eashansinha eashansinha commented Jun 18, 2026

Copy link
Copy Markdown

Summary

Full rewrite of the Angular 9 Hacker News PWA to React 18 + Vite + TypeScript + React Router v6, preserving all features, routes, the 3-theme engine, and PWA/offline behavior. The Angular toolchain (@angular/*, angular.json, Karma, yarn.lock) is removed in favor of Vite + vite-plugin-pwa.

The migration was parallelized: I built a shared foundation, then 4 sessions each ported a disjoint slice on top of it (each merged via its own PR into this branch):

What changed

Architecture mapping (Angular → React):

  • NgModule + lazy routes → BrowserRouter + <Routes> in App.tsx
  • RxJS HttpClient service → async/await fetch service src/services/hackerNewsApi.ts
  • SettingsService + BehaviorSubjectSettingsContext + useSettings() hook (localStorage persistence + prefers-color-scheme listener)
  • @Input() → props; [innerHTML]dangerouslySetInnerHTML; routerLink/routerLinkActive<Link>/<NavLink>
  • Component data fetching → useState + useEffect with a cancelled flag for cleanup/stale-request guarding
  • Theme/global SCSS engine ported verbatim (class names preserved so theming still applies); per-component SCSS imported for side effects (global, not CSS modules)

Routing (unchanged behavior):

/                 -> redirect to /news/1
/:feedType/:page  -> Feed   (feedType ∈ news|newest|show|ask|jobs)
/item/:id         -> ItemDetails (+ recursive Comment, polls)
/user/:id         -> User

API servicefetchItemContent keeps the original poll-resolution logic (fetch each poll option, sum poll_votes_count), now guarded against missing arrays / divide-by-zero in the poll bars:

if (story.type === 'poll' && Array.isArray(story.poll)) {
  const results = await Promise.all(
    story.poll.map((_, i) => fetchPollContent(story.id + i + 1)),
  );
  results.forEach((r, i) => { story.poll[i] = r; story.poll_votes_count += r.points; });
}

PWAvite-plugin-pwa (registerType: 'autoUpdate') reproduces the manifest + service worker, with NetworkFirst runtime caching for node-hnapi.herokuapp.com so the app still works offline.

Branding — visible "Angular 2 HN" surfaces updated to "React HN".

Notes / faithful-port decisions

  • Deleted comments omit their subtree, matching the original Angular *ngIf="comment.deleted" behavior (intentionally not changed).
  • Dart Sass emits @import / legacy-API deprecation warnings; these do not fail the build. Left as-is to keep the theme SCSS a verbatim port; can be modernized to @use in a follow-up.

Verification

npm run lint (max-warnings 0), npm run typecheck, and npm run build all pass on the integrated branch. End-to-end desktop + mobile testing of the running app follows in the PR comments.

Link to Devin session: https://app.devin.ai/sessions/353da171a8b64dcdb63dbfe4c6ffe4e7
Requested by: @eashansinha


Devin Review

Status Commit
🟢 Reviewed 569bfc1
Open in Devin Review (Staging)

devin-ai-integration Bot and others added 10 commits June 18, 2026 17:26
Replace the Angular 9 app with a React 18 + Vite + TypeScript foundation that
later feature slices build on top of:

- Project config: Vite, vite-plugin-pwa, ESLint, TS project references
- Shared contracts: TS types, HN API service (async), Settings context/provider
- Global SCSS theme engine (default/night/amoledblack) ported verbatim
- Routing wired in App.tsx; Loader + ErrorMessage implemented
- Placeholder Header/Footer/Feed/ItemDetails/User to be filled by feature work

Move static assets to public/, remove Angular tooling/config.

Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Port UserComponent to src/features/user/User.tsx and User.scss.
Fetch the user via fetchUser(id) with cancel-on-unmount handling,
show Loader while pending and ErrorMessage on failure, and render
the profile (id, karma, created, about HTML) preserving the original
Angular class names for the global theme engine.

Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Port HeaderComponent, FooterComponent, and SettingsComponent from Angular to React 18 + TypeScript, wired to the shared SettingsContext. Preserves original class names for the global theme engine; rebrands to React HN.

Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
…o React

Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Restores Angular *ngFor's no-crash behavior when item.poll is absent, and
avoids NaN% poll-bar width when poll_votes_count is 0.

Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
…me' into devin/1781803206-react-migration-base
…ils' into devin/1781803206-react-migration-base
@devin-ai-integration

Copy link
Copy Markdown
Original prompt from Eashan

migrate this angular repo to react. spin up multiple devin sessions to do this

@devin-ai-integration

Copy link
Copy Markdown

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

staging-devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration

Copy link
Copy Markdown

End-to-end test results

Ran the migrated React app locally (http://localhost:5173, React 18 + Vite + TS) and exercised every user-facing flow via the browser GUI — desktop + mobile emulation. Annotated recording below. Full report + per-test screenshots in the collapsed sections.

# Test Result
T1 Feed list + pagination ✅ Passed
T2 Item details + recursive comments + collapse ✅ Passed
T3 Poll rendering (proportional bars) ✅ Passed
T4 User profile happy-path ⚠️ Untested (upstream API 404)
T5 Settings: theme switch + persistence + open-links ✅ Passed
T6 Mobile responsive (~400px) + back-arrow ✅ Passed

Recording (annotated)

recording

⚠️ Caveats to flag

  • T4 user profile is unverified end-to-end. Upstream node-hnapi /user/:id returns 404 for every username — this is not a regression (the original Angular app hits the same dead endpoint). The React app handles it gracefully with an error page instead of crashing; only the happy-path render (karma ★ + created date) is unverified because the API serves no user data.
  • "Open links in new tab": clicking via the automation harness opened same-tab, but that's a harness artifact (it strips target="_blank"). A clean Playwright navigation confirmed the app does emit target="_blank" + rel="noopener noreferrer" when the setting is on.
T1 — Feed list + pagination

//news/1, 30 ranked rows. More ›/news/2 with first rank = 31 (listStart = (page-1)*30+1).

T1

T2 — Recursive comments + collapse

Comments nest multiple levels deep. [-][+] hides the comment body + its whole subtree; clicking restores it.

T2

T3 — Poll proportional bars (/item/126809)

3 options render; bar widths proportional to points (179 widest, 49 shortest), no NaN.

T3

T4 — User profile (untested, upstream 404)

Graceful error page on the upstream 404 — no crash. Happy-path unverified.

T4

T5 — Settings: theme + persistence + open-links

Overlay opens with Links checkbox, 3 theme radios, font/spacing inputs. Night theme applies immediately and persists across reload (localStorage).

T5 panel
T5 theme

T6 — Mobile responsive (~400px)

Feed switches to subtext-palm layout, no horizontal overflow. Item page shows mobile back-arrow header (navigates back); poll + nested comments render.

T6

— Tested by Devin · session

devin-ai-integration Bot and others added 2 commits June 18, 2026 18:21
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>

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

View 3 additional findings in Devin Review.

Open in Devin Review (Staging)
Debug

Playground

devin-ai-integration[bot]

This comment was marked as resolved.

…st-visit theme flash

Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>

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

View 4 additional findings in Devin Review.

Open in Devin Review (Staging)
Debug

Playground

Comment thread src/App.tsx
<Route path="/" element={<Navigate to="/news/1" replace />} />
<Route path="/item/:id" element={<ItemDetails />} />
<Route path="/user/:id" element={<User />} />
<Route path="/:feedType/:page" element={<Feed />} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Catch-all feed route accepts arbitrary feed types unlike the old explicit routes

The route /:feedType/:page at src/App.tsx:22 accepts any two-segment URL path (e.g. /random/1 or /foo/bar), whereas the old Angular router only defined explicit feed routes for 'news', 'newest', 'show', 'ask', and 'jobs'. Navigating to an invalid feed type will hit the API with an unsupported endpoint, returning an error that is caught and displayed — so it won't crash. However, this is a behavioral change: the old app would have shown no match for invalid feed paths, while the new app shows a "Could not load" error. Consider whether this is acceptable or if the route should be constrained (e.g. /:feedType(news|newest|show|ask|jobs)/:page).

Open in Devin Review (Staging)

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

Debug

Playground

@devin-ai-integration 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.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +29 to +31
const handleChange = (event: MediaQueryListEvent | MediaQueryList) => {
setTheme(event.matches ? 'night' : 'default');
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 System theme change listener overwrites user-selected themes (carried over from Angular)

The handleChange event listener at line 30 calls setTheme, which writes to localStorage (src/context/SettingsProvider.tsx:22). If a user manually selects the 'amoledblack' theme via Settings, and then their OS toggles between light/dark mode, the change event fires and overwrites the user's manually selected theme with either 'night' or 'default', persisting this to localStorage. This is the same behavior as the old Angular code (src/app/shared/services/settings.service.ts:28-35), but notably the new code actually makes it more effective — the old Angular code used .bind(this) in both addEventListener and removeEventListener, creating different function references each time, which meant the listener was never properly removed. The new code correctly uses the same handleChange reference, so the listener properly registers and cleans up.

Open in Devin Review

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Intentionally leaving this as-is — it's a faithful port of the original Angular behavior (settings.service.ts:28-35, where handleSystemPreferredColorSchemeChangesetTheme persists to localStorage), and this PR's goal is a behavior-preserving migration. Changing it (e.g. only auto-applying the OS scheme when no theme was manually chosen) would be a deliberate product/behavior change, which I'd rather not slip into the migration PR.

Good observation on the listener cleanup though: the React version uses a single stable handleChange reference so removeEventListener actually works, whereas the Angular version's .bind(this) created a new reference each call and never detached. That's a (positive) side effect of the migration, not an intentional behavior change. Happy to open a follow-up if you'd like to change the override semantics.

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