Migrate Angular 9 HN PWA to React 18 + Vite + TypeScript#357
Migrate Angular 9 HN PWA to React 18 + Vite + TypeScript#357eashansinha wants to merge 13 commits into
Conversation
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
…to devin/1781803206-react-migration-base
Original prompt from Eashan
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
End-to-end test resultsRan the migrated React app locally (
Recording (annotated)
|
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
…st-visit theme flash Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
| <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 />} /> |
There was a problem hiding this comment.
🚩 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).
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
| const handleChange = (event: MediaQueryListEvent | MediaQueryList) => { | ||
| setTheme(event.matches ? 'night' : 'default'); | ||
| }; |
There was a problem hiding this comment.
🚩 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Intentionally leaving this as-is — it's a faithful port of the original Angular behavior (settings.service.ts:28-35, where handleSystemPreferredColorSchemeChange → setTheme 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.
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>inApp.tsxHttpClientservice →async/awaitfetch servicesrc/services/hackerNewsApi.tsSettingsService+BehaviorSubject→SettingsContext+useSettings()hook (localStoragepersistence +prefers-color-schemelistener)@Input()→ props;[innerHTML]→dangerouslySetInnerHTML;routerLink/routerLinkActive→<Link>/<NavLink>useState+useEffectwith acancelledflag for cleanup/stale-request guardingRouting (unchanged behavior):
API service —
fetchItemContentkeeps the original poll-resolution logic (fetch each poll option, sumpoll_votes_count), now guarded against missing arrays / divide-by-zero in the poll bars:PWA —
vite-plugin-pwa(registerType: 'autoUpdate') reproduces the manifest + service worker, withNetworkFirstruntime caching fornode-hnapi.herokuapp.comso the app still works offline.Branding — visible "Angular 2 HN" surfaces updated to "React HN".
Notes / faithful-port decisions
*ngIf="comment.deleted"behavior (intentionally not changed).@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@usein a follow-up.Verification
npm run lint(max-warnings 0),npm run typecheck, andnpm run buildall 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
569bfc1