Migrate from Angular 9 to React 18 + TypeScript + Vite#433
Migrate from Angular 9 to React 18 + TypeScript + Vite#433devanshi-gpta wants to merge 8 commits into
Conversation
…ttingsContext, SCSS, routing, PWA Co-Authored-By: Devanshi Gupta <devanshi.gupta@cognition.ai>
Co-Authored-By: Devanshi Gupta <devanshi.gupta@cognition.ai>
Co-Authored-By: Devanshi Gupta <devanshi.gupta@cognition.ai>
Co-Authored-By: Devanshi Gupta <devanshi.gupta@cognition.ai>
Co-Authored-By: Devanshi Gupta <devanshi.gupta@cognition.ai>
…, ItemDetails/Comment, User)
…eact migration Co-Authored-By: Devanshi Gupta <devanshi.gupta@cognition.ai>
🤖 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:
|
| checked={settings.theme === 'default'} | ||
| onClick={() => setTheme('default')} |
There was a problem hiding this comment.
🟡 Theme radio buttons produce React warnings and may not visually update on click
The theme radio buttons are set as controlled inputs with checked but use onClick instead of onChange (onClick={() => setTheme('default')} at src/components/Settings/Settings.tsx:36), so React logs a console warning about a missing onChange handler on every render of the settings panel.
Impact: Console warnings clutter developer tools, and the radio button's visual checked state may briefly lag behind the actual state.
Controlled radio inputs require onChange in React
In React, when you provide a checked prop to a radio input, it becomes a "controlled component." React expects a corresponding onChange handler to manage state changes. Without it, React logs: "You provided a checked prop to a form field without an onChange handler."
The three radio buttons at src/components/Settings/Settings.tsx:35-36, src/components/Settings/Settings.tsx:48-49, and src/components/Settings/Settings.tsx:59-60 all use checked={...} with onClick instead of onChange. While onClick does fire and update state, the idiomatic React pattern is onChange.
The fix is to replace onClick with onChange on all three radio inputs:
onChange={() => setTheme('default')}
Prompt for agents
In src/components/Settings/Settings.tsx, three radio button inputs (at approximately lines 35-36, 48-49, and 59-60) use the checked prop (making them controlled) with onClick handlers instead of onChange handlers. React expects controlled form inputs to use onChange. Replace onClick with onChange on all three radio inputs to eliminate the React console warning. For example, change onClick={() => setTheme('default')} to onChange={() => setTheme('default')} on each of the three theme radio buttons.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in de4b3ae — replaced onClick with onChange on all three theme radio inputs so the controlled inputs no longer trigger React's missing-onChange warning.
| a YC startup through <a href="https://triplebyte.com/?ref=yc_jobs">Triplebyte</a>. | ||
| </p> | ||
| )} | ||
| {feedType !== 'new' && ( |
There was a problem hiding this comment.
🔍 Dead-code condition carried over from Angular: feedType is never 'new'
At src/components/Feed/Feed.tsx:58, the condition feedType !== 'new' is always true because the feed types passed via routes are 'news', 'newest', 'show', 'ask', and 'jobs' — none of which equal 'new'. This was the same in the old Angular template (src/app/feeds/feed/feed.component.html:10), where the route data used feedType: 'newest'. The <ol> is therefore always rendered. This is pre-existing dead code that was faithfully migrated, but it's confusing and could be cleaned up or the intent clarified.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Acknowledged — this is a faithful 1:1 port of the original Angular template (feedType !== 'new', where the route feed types are news/newest/show/ask/jobs), so the <ol> always renders. Leaving it as-is to keep the migration behavior-preserving; happy to clean up the dead condition in a follow-up if desired.
| const handleChange = (event: MediaQueryListEvent) => { | ||
| setTheme(event.matches ? 'night' : 'default'); |
There was a problem hiding this comment.
📝 Info: System dark mode changes always override user's manually selected theme
At src/context/SettingsContext.tsx:68-69, the handleChange listener for prefers-color-scheme always calls setTheme(event.matches ? 'night' : 'default'), which overwrites both the in-memory state and localStorage. If a user manually selects the 'amoledblack' theme and then their OS dark mode toggles, their choice is lost. The old Angular SettingsService had the same behavior (handleSystemPreferredColorSchemeChange called this.setTheme(theme)), so this is pre-existing. A fix would be to only auto-switch if the user hasn't manually chosen a theme (e.g., by tracking a userOverride flag).
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Acknowledged — as noted, this mirrors the original Angular SettingsService.handleSystemPreferredColorSchemeChange, so it's pre-existing behavior preserved by the migration. Adding a userOverride flag would be a behavior change beyond the migration scope; leaving as-is unless the maintainers want the enhancement.
| <meta name="apple-mobile-web-app-capable" content="yes"> | ||
| <meta name="apple-mobile-web-app-status-bar-style" content="black"> | ||
|
|
||
| <link rel="manifest" href="/manifest.json"> |
There was a problem hiding this comment.
🔍 Potential duplicate manifest links: static manifest.json and VitePWA-generated manifest
The index.html:35 has <link rel="manifest" href="/manifest.json"> pointing to the static file in public/manifest.json. Meanwhile, vite-plugin-pwa (configured in vite.config.ts:9-63) with a manifest option will generate its own manifest.webmanifest and inject a second <link rel="manifest"> tag into the built HTML. The browser will use the first manifest link it encounters. The two manifests currently have similar content, but maintaining both is fragile — changes to the VitePWA config won't take effect if the static manifest link comes first. Consider removing either the static public/manifest.json + the HTML link, or the manifest option from VitePWA config.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in de4b3ae. Confirmed the built dist/index.html previously emitted two <link rel="manifest"> tags (static /manifest.json + vite-plugin-pwa's /manifest.webmanifest). Set manifest: false in the VitePWA config so the plugin no longer generates/injects its own manifest; the static public/manifest.json (kept unchanged for Firebase Hosting) is now the single source of truth. The built HTML now contains exactly one manifest link.
| <meta name="description" content="A Hacker News client built with Angular CLI, RxJS and Webpack"/> | ||
|
|
||
| <meta name="twitter:card" content="summary"> | ||
| <meta name="twitter:site" content="@hdjirdeh"> | ||
| <meta name="twitter:title" content="Angular 2 HN"> | ||
| <meta name="twitter:description" content="A Hacker News client built with Angular CLI, RxJS and Webpack"> | ||
| <meta name="twitter:creator" content="@hdjirdeh"> | ||
| <meta name="twitter:image" content="assets/images/logo-loading.png"> | ||
|
|
||
| <meta property="og:title" content="Angular 2 HN"/> | ||
| <meta property="og:type" content="website"/> | ||
| <meta property="og:url" content="https://angular2-hn.firebaseapp.com/"/> | ||
| <meta property="og:image" content="assets/images/logo-loading.png"/> | ||
| <meta property="og:description" content="A Hacker News client built with Angular CLI, RxJS and Webpack"/> |
There was a problem hiding this comment.
📝 Info: HTML meta descriptions still reference Angular CLI, RxJS, and Webpack
The index.html meta tags at lines 7, 12, and 20 still describe the app as "A Hacker News client built with Angular CLI, RxJS and Webpack". Since the project has been migrated to React + Vite, these descriptions are now inaccurate. The Twitter and OpenGraph metadata will show outdated technology references when the page is shared on social media.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in de4b3ae — updated the description, twitter:description, and og:description meta tags in index.html to "A Hacker News client built with React, TypeScript and Vite".
| </div> | ||
| <div className="comment-tree"> | ||
| <div style={{ display: collapse ? 'none' : undefined }}> | ||
| <p className="comment-text" dangerouslySetInnerHTML={{ __html: comment.content }} /> |
There was a problem hiding this comment.
🟥 Unsanitized HTML rendering loses Angular's built-in XSS protection in comment content
The migration from Angular to React replaces Angular's [innerHTML] (which auto-sanitizes dangerous HTML) with React's dangerouslySetInnerHTML (src/components/Comment/Comment.tsx:36), which performs NO sanitization. Comment content from the third-party HN API (node-hnapi.herokuapp.com) is rendered as raw HTML. If the API returns or is manipulated to return malicious HTML (e.g., <img onerror="...">, <svg onload="...">), it will execute in the user's browser.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Valid observation. Angular's [innerHTML] auto-sanitizes via DomSanitizer, whereas React's dangerouslySetInnerHTML does not — so this is a real behavioral difference. However, the migration spec explicitly mapped [innerHTML] → dangerouslySetInnerHTML, and the content comes from the HN API (node-hnapi). Rather than unilaterally add a sanitizer (new dependency + deviation from the requested mapping), I'm flagging this to the requester to decide whether to add DOMPurify here (and in ItemDetails/User). Not changing in this PR pending that decision.
| <div dangerouslySetInnerHTML={{ __html: pollResult.content }} /> | ||
| <div className="subtext">{pollResult.points} points</div> | ||
| <div | ||
| className="pollBar" | ||
| style={{ width: `${(pollResult.points / item.poll_votes_count) * 100}%` }} | ||
| /> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| )} | ||
| <p className="subject" dangerouslySetInnerHTML={{ __html: item.content }} /> |
There was a problem hiding this comment.
🟥 Unsanitized HTML rendering of item and poll content enables XSS
The ItemDetails component renders both item.content and pollResult.content from the third-party API using dangerouslySetInnerHTML (src/components/ItemDetails/ItemDetails.tsx:122 and src/components/ItemDetails/ItemDetails.tsx:132) without any HTML sanitization, allowing potential script injection if the API returns malicious markup.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Same class of issue as the comment content in Comment.tsx — Angular's [innerHTML] sanitized, React's dangerouslySetInnerHTML does not. This was the spec's explicit mapping and the source is the HN API. Flagging to the requester to decide on adding DOMPurify across all three render sites (Comment, ItemDetails item/poll content, User about) as a single consistent change rather than deviating from the requested migration here.
| </div> | ||
| {user.about && ( | ||
| <div className="other-details"> | ||
| <p dangerouslySetInnerHTML={{ __html: user.about }}></p> |
There was a problem hiding this comment.
🟥 Unsanitized HTML rendering of user profile 'about' field enables XSS
The User component renders user.about from the third-party API using dangerouslySetInnerHTML (src/components/User/User.tsx:63) without sanitization. User profile 'about' fields on Hacker News can contain HTML, and without sanitization, any malicious content would execute in the viewer's browser.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Same as the other two dangerouslySetInnerHTML sites. Deferring to the requester on whether to introduce DOMPurify for the about field; if approved I'll sanitize all three render sites consistently in one change.
… update index.html meta to React Co-Authored-By: Devanshi Gupta <devanshi.gupta@cognition.ai>
End-to-end test resultsRan the migrated app locally (Vite dev server) against the live node-hnapi and exercised the primary flows in the browser; also verified the PWA-manifest and meta-description fixes in the production build. Escalations first:
Results:
Theme switching — onChange fix (before → after)
Feed, pagination, item details, comment collapse
Tested via this Devin session. |
Summary
Full migration of the Hacker News PWA from Angular 9 (with Webpack, RxJS, zone.js) to React 18 + TypeScript + Vite + react-router-dom v6.
Toolchain replacement:
angular.json/ Webpack /@angular/cli→vite.config.tswith@vitejs/plugin-react@angular/service-worker/ngsw-config.json→vite-plugin-pwa(WorkboxgenerateSW,NetworkFirstruntime caching for the HN API,navigateFallbackfor app-shell)tslint.json/codelyzer→ ESLint with@typescript-eslint+react-hooks+react-refreshpluginskarma/protractor/jasmine→ removed (no test framework added — matching original repo which had no passing tests)Architecture:
HackerNewsAPIService(RxJS observables,lazyFetchwith cancel tokens) →src/api/hackerNewsApi.tsexportingfetchFeed,fetchItemContent,fetchPollContent,fetchUseras async functions usingfetch+AbortController. Poll logic usesPromise.allinstead of nested subscriptions.SettingsService(injectable singleton) →SettingsContext/SettingsProviderwithuseSettings()hook. PreserveslocalStoragepersistence andprefers-color-schemedark-mode listener.CommentPipe→commentLabel()utility insrc/utils/comment.tsStory,User,Comment,Settings,PollResult,FeedType) kept as plain TS interfacesRouting (
react-router-domv6 nested routes):Component porting (Angular → React function components):
AppComponent→App: themeclassNameon root div, GA pageview tracking viauseLocationeffectFeedComponent→Feed:useEffectfetch withAbortControllercleanup, prev/more pagination via<Link>ItemComponent→Item: consumesuseSettings()for font size, spacing, open-in-new-tabItemDetailsComponent→ItemDetails: poll results withdangerouslySetInnerHTML, recursiveCommenttreeCommentComponent→Comment: collapse toggle viauseState, recursive rendering, deleted-comment handlingUserComponent→User: profile withdangerouslySetInnerHTMLforaboutHeaderComponent→Header:NavLinks with scroll-to-top, settings cog toggleSettingsComponent→Settings: theme radios, font/spacing inputs, open-links checkbox — all wired to contextSCSS: All existing SCSS files (themes, per-component styles) preserved and imported; theme class-name approach unchanged.
PWA:
index.htmlkeeps all meta tags, manifest link, GA script, noscript fallback; root mount changed from<app-root>to<div id="root">.Removed:
angular.json,karma.conf.js,tslint.json,ngsw-config.json,tsconfig.app.json,tsconfig.spec.json,e2e/,src/app/,src/main.ts,src/polyfills.ts,src/test.ts,src/environments/,yarn.lock.Updated: README build instructions reflect Vite commands;
.travis.ymldrops@angular/cliinstall, bumps to Node 20.Link to Devin session: https://app.devin.ai/sessions/d7689858b25b4d4f9a42e953b3156cbf
Requested by: @devanshi-gpta
Devin Review