Skip to content

feature: Migrate Angular 9 PWA to React 18 + TypeScript + Vite#318

Open
devin-ai-integration[bot] wants to merge 2 commits into
masterfrom
devin/1781027650-react-migration
Open

feature: Migrate Angular 9 PWA to React 18 + TypeScript + Vite#318
devin-ai-integration[bot] wants to merge 2 commits into
masterfrom
devin/1781027650-react-migration

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown

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 deploys react-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.

Running React app — news feed

Key architectural changes (Angular → React)

  • HTTP/RxJS → fetch + async/await. Observable-based lazy fetch wrappers became fetch calls cancelled via AbortController in useEffect cleanup (prevents state updates after unmount):
    useEffect(() => {
      const ac = new AbortController();
      getFeed(type, page, ac.signal).then(setStories).catch(ignoreAbort);
      return () => ac.abort();
    }, [type, page]);
  • SettingsServiceSettingsContext. Theme state persists to localStorage and the window.matchMedia('(prefers-color-scheme: dark)') listener for system theme is preserved. Theme is applied as a className on the top-level layout, matching the original [ngClass]="settings.theme".
  • Angular pipes → src/utils/ plain functions (e.g. comment/time formatting).
  • Components/pages → function components + hooks; *ngIf/*ngFor/[prop]/(click) mapped to JSX/map/props/handlers. [innerHTML] (comment bodies) → dangerouslySetInnerHTML, only where Angular already used it.
  • Lazy NgModules → React Router v7 nested routes under a shared Layout shell (header/footer). Same paths: /news/:page, /newest/:page, /show/:page, /ask/:page, /jobs/:page, /item/:id, /user/:id, with / redirecting to /news/1.
  • PWA via vite-plugin-pwa (generateSW): ported manifest + icons, Workbox precaching, and NetworkFirst runtime caching for node-hnapi.herokuapp.com.

Bug fixes

  • Asset paths. Header logo/cog and HTML icons used relative assets/... paths, which 404'd when a nested route like /news/1 was hard-loaded/refreshed (resolved to /news/assets/...). Switched to absolute /assets/... in Header.tsx, index.html, and the PWA manifest icons so they load on every route.
  • lazyFetch error handling (from review): now checks res.ok and throws on 4xx/5xx, so feed/item/user pages render their error state instead of passing a non-array body into items.map(...) (a render-time crash). The original Angular wrapper had the same gap.
  • Story.time_ago type (from review): changed number → string to match the actual node-hnapi response and the sibling Comment.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.
  • CI on this PR: Snyk security, Snyk license, and Devin Review all green.

Notes

  • External API endpoints and data contracts are unchanged. No new UI libraries introduced.
  • GET /user/:id on the upstream node-hnapi service 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

Status Commit
🟢 Reviewed 9f28e13
Open in Devin Review (Staging)

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

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

Open in Devin Review (Staging)
Debug

Playground

Comment thread react-app/src/services/hackernews-api.ts
Comment thread react-app/src/types/index.ts Outdated
Comment on lines +50 to +67
useEffect(() => {
const media = darkColorSchemeMedia.current;

const handleSystemPreferredColorSchemeChange = (
event: MediaQueryListEvent
) => {
setTheme(event.matches ? 'night' : 'default');
};

media.addEventListener('change', handleSystemPreferredColorSchemeChange);

return () => {
media.removeEventListener(
'change',
handleSystemPreferredColorSchemeChange
);
};
}, [setTheme]);

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

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
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/services/hackernews-api.ts
Comment thread react-app/src/types/index.ts Outdated
Comment on lines +50 to +67
useEffect(() => {
const media = darkColorSchemeMedia.current;

const handleSystemPreferredColorSchemeChange = (
event: MediaQueryListEvent
) => {
setTheme(event.matches ? 'night' : 'default');
};

media.addEventListener('change', handleSystemPreferredColorSchemeChange);

return () => {
media.removeEventListener(
'change',
handleSystemPreferredColorSchemeChange
);
};
}, [setTheme]);

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.

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

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.

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>

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

View 4 additional findings in Devin Review.

Open in Devin Review

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

Open in Devin Review (Staging)
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