feature: Migrate Angular 9 PWA to React 18 + TypeScript + Vite#318
feature: Migrate Angular 9 PWA to React 18 + TypeScript + Vite#318devin-ai-integration[bot] wants to merge 2 commits into
Conversation
Parallel rewrite of the Hacker News client from Angular to React, preserving all functionality: routing (same URL structure), 3-theme switching, API services, and PWA support (service worker + manifest). Angular source removed after the React app fully builds and all pages verified. - Types ported to src/types; HttpClient/RxJS replaced with fetch + AbortController - SettingsService -> React Context (localStorage + matchMedia preserved) - Angular pipes -> utils; components/pages -> function components with hooks - React Router v7 nested routes under a shared Layout shell - vite-plugin-pwa for manifest + Workbox precaching/runtime caching - Vitest + React Testing Library tests; Travis/firebase updated to react-app/dist - Assets referenced with absolute /assets paths so header/icons load on nested routes Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
🤖 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:
|
| useEffect(() => { | ||
| const media = darkColorSchemeMedia.current; | ||
|
|
||
| const handleSystemPreferredColorSchemeChange = ( | ||
| event: MediaQueryListEvent | ||
| ) => { | ||
| setTheme(event.matches ? 'night' : 'default'); | ||
| }; | ||
|
|
||
| media.addEventListener('change', handleSystemPreferredColorSchemeChange); | ||
|
|
||
| return () => { | ||
| media.removeEventListener( | ||
| 'change', | ||
| handleSystemPreferredColorSchemeChange | ||
| ); | ||
| }; | ||
| }, [setTheme]); |
There was a problem hiding this comment.
🚩 System color scheme change overrides user's explicit theme choice
The useEffect at line 50-67 subscribes to prefers-color-scheme media query changes and unconditionally calls setTheme(), which also writes to localStorage. If a user has manually selected 'amoledblack' theme, and then their OS toggles between light/dark mode, the user's choice is overwritten with either 'night' or 'default'. This was the same behavior in the old Angular SettingsService (src/app/shared/services/settings.service.ts:28-36), so it's a pre-existing design issue carried over to the migration. A fix would be to check localStorage.getItem('theme') before overriding, or to track whether the user has explicitly set a theme.
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
| useEffect(() => { | ||
| const media = darkColorSchemeMedia.current; | ||
|
|
||
| const handleSystemPreferredColorSchemeChange = ( | ||
| event: MediaQueryListEvent | ||
| ) => { | ||
| setTheme(event.matches ? 'night' : 'default'); | ||
| }; | ||
|
|
||
| media.addEventListener('change', handleSystemPreferredColorSchemeChange); | ||
|
|
||
| return () => { | ||
| media.removeEventListener( | ||
| 'change', | ||
| handleSystemPreferredColorSchemeChange | ||
| ); | ||
| }; | ||
| }, [setTheme]); |
There was a problem hiding this comment.
🚩 System color scheme change listener overrides user-selected theme and persists it to localStorage
The useEffect at react-app/src/context/SettingsContext.tsx:50-67 subscribes to OS color scheme changes and calls setTheme() which both updates state AND writes to localStorage (react-app/src/context/SettingsContext.tsx:44-47). If a user explicitly selects 'amoledblack' theme and then their OS toggles between light/dark mode, the user's choice is overwritten to 'night' or 'default' and saved permanently. The handler should arguably check whether a user has explicitly chosen a theme (e.g., via a separate localStorage flag) before overriding. This was the same behavior in the old Angular SettingsService (src/app/shared/services/settings.service.ts:28-35), so it was faithfully ported, but it's still a UX issue worth addressing.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Acknowledged — and as the finding notes, this is a faithful 1:1 port of the original Angular SettingsService (src/app/shared/services/settings.service.ts), which also let an OS color-scheme change overwrite the user's selected theme and persist it. Since this migration is intended as a behavior-preserving rewrite (and the playbook forbids changing behavior/data contracts), I've kept it as-is to avoid drift. It's a legitimate pre-existing UX issue worth a dedicated follow-up — e.g. tracking an explicit "user chose a theme" flag so the system listener only applies when the user hasn't made a manual choice. Happy to do that in a separate PR if you'd like.
- lazyFetch now checks res.ok and throws on 4xx/5xx so feed/item/user error states render instead of passing a non-array body to items.map (runtime crash) - Story.time_ago typed as string (API returns a human-readable string, matching the Comment interface); test fixture updated accordingly Co-Authored-By: Lukas Burger <lukaskburger@gmail.com>
Summary
Full parallel rewrite of this Hacker News client from Angular 9 → React 18 + TypeScript + Vite, preserving all user-facing functionality: routing (identical URL structure), all 3 themes, the HN API integration, and PWA support (service worker + manifest). The Angular source was only removed after the React app fully built and every page was verified in the browser.
The new app source lives under
react-app/. CI/CD (.travis.yml,firebase.json) now builds and deploysreact-app/dist.Verified locally (Vite dev server): all feeds (news / newest / show / ask / jobs), pagination, item detail pages with the nested comment tree, theme switching (default / night / amoledblack, persisted to
localStorage), and live data loading from the HN API.Key architectural changes (Angular → React)
fetchcalls cancelled viaAbortControllerinuseEffectcleanup (prevents state updates after unmount):SettingsService→SettingsContext. Theme state persists tolocalStorageand thewindow.matchMedia('(prefers-color-scheme: dark)')listener for system theme is preserved. Theme is applied as aclassNameon the top-level layout, matching the original[ngClass]="settings.theme".src/utils/plain functions (e.g. comment/time formatting).*ngIf/*ngFor/[prop]/(click)mapped to JSX/map/props/handlers.[innerHTML](comment bodies) →dangerouslySetInnerHTML, only where Angular already used it.Layoutshell (header/footer). Same paths:/news/:page,/newest/:page,/show/:page,/ask/:page,/jobs/:page,/item/:id,/user/:id, with/redirecting to/news/1.vite-plugin-pwa(generateSW): ported manifest + icons, Workbox precaching, andNetworkFirstruntime caching fornode-hnapi.herokuapp.com.Bug fixes
assets/...paths, which 404'd when a nested route like/news/1was hard-loaded/refreshed (resolved to/news/assets/...). Switched to absolute/assets/...inHeader.tsx,index.html, and the PWA manifest icons so they load on every route.lazyFetcherror handling (from review): now checksres.okand throws on 4xx/5xx, so feed/item/user pages render their error state instead of passing a non-array body intoitems.map(...)(a render-time crash). The original Angular wrapper had the same gap.Story.time_agotype (from review): changednumber → stringto match the actual node-hnapi response and the siblingComment.time_ago: string.Testing / build
npm run build— succeeds, zero TypeScript errors (PWA service worker + manifest generated).npm run lint— clean.npm test(Vitest + React Testing Library) — 5/5 passing: a utility test, a component render test, and a page-level integration test.Notes
GET /user/:idon the upstreamnode-hnapiservice returns 404 (the endpoint is gone upstream — same result against the original Angular code), so the user profile page shows the error state. This is preserved behavior, not a regression.Link to Devin session: https://app.devin.ai/sessions/794f870a47df4c99a8c7f837e394567f
Requested by: @lburgers
Devin Review
9f28e13