<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Katie</title>
    <description>The latest articles on Forem by Katie (@notjustsyntax).</description>
    <link>https://forem.com/notjustsyntax</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3847261%2F619ae745-44ee-4c65-b823-9002a1b11760.png</url>
      <title>Forem: Katie</title>
      <link>https://forem.com/notjustsyntax</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/notjustsyntax"/>
    <language>en</language>
    <item>
      <title>From Vanilla JS to React: React Makes More Sense When the Problem Comes First</title>
      <dc:creator>Katie</dc:creator>
      <pubDate>Sat, 28 Mar 2026 08:10:19 +0000</pubDate>
      <link>https://forem.com/notjustsyntax/from-vanilla-js-to-react-react-makes-more-sense-when-the-problem-comes-first-2k2j</link>
      <guid>https://forem.com/notjustsyntax/from-vanilla-js-to-react-react-makes-more-sense-when-the-problem-comes-first-2k2j</guid>
      <description>&lt;p&gt;I had an e-commerce project — plain HTML/JavaScript frontend, Node.js/Express backend. I took on a more interesting challenge: migrating the frontend from vanilla HTML/JS to React.&lt;/p&gt;

&lt;p&gt;Migrating code you already wrote and understand is one of the better ways to go deeper with React. You're not learning the syntax and the concepts at the same time — you already know what the code does, so you can focus entirely on &lt;em&gt;why&lt;/em&gt; each React pattern exists. This is what that looked like.&lt;/p&gt;




&lt;h2&gt;
  
  
  The original version
&lt;/h2&gt;

&lt;p&gt;Each page was a separate HTML file loaded its own JS modules. State lived in the DOM. Navigation caused full page reloads. The patterns were familiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;updateCartIcon()&lt;/code&gt; fetches the cart total from the backend and updates the cart icon in the banner — it had to be manually called in every place that could affect the cart count&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;showHideMenuItems()&lt;/code&gt; checks if the user is logged in and toggles the visibility of login, signup, and logout buttons by directly setting &lt;code&gt;element.style.display&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Any change to the shared header or footer meant touching every HTML file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are bugs, but they're all symptoms of the same root problem: &lt;strong&gt;state is scattered and has to be manually synchronized&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pages → components
&lt;/h2&gt;

&lt;p&gt;The mapping is direct. Each HTML file becomes a page component, and React Router replaces file-based navigation. &lt;code&gt;window.location.href = '/cart.html'&lt;/code&gt; becomes &lt;code&gt;navigate('/cart')&lt;/code&gt;. &lt;code&gt;&amp;lt;a href="/login.html"&amp;gt;&lt;/code&gt; becomes &lt;code&gt;&amp;lt;Link to="/login"&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The more meaningful difference is that only the page component swaps out on navigation — the rest of the component tree stays mounted, so state persists across the session without any extra work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Extracting shared UI
&lt;/h2&gt;

&lt;p&gt;Every page shared the same banner, header, and footer. In the old app this was solved with copy-paste. In React, a &lt;code&gt;Layout&lt;/code&gt; component wraps all routes via React Router's &lt;code&gt;&amp;lt;Outlet /&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layout
├── Banner
├── Header
├── &amp;lt;Outlet /&amp;gt;   ← current page renders here
└── Footer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Banner itself had three distinct responsibilities — a mobile menu toggle, auth-aware navigation links, and a cart icon — so it was broken into &lt;code&gt;Banner&lt;/code&gt;, &lt;code&gt;Navigation&lt;/code&gt;, and &lt;code&gt;CartIcon&lt;/code&gt; components.&lt;/p&gt;

&lt;p&gt;The toggle was the first real state decision: the button lives in &lt;code&gt;Banner&lt;/code&gt;, and &lt;code&gt;Navigation&lt;/code&gt; receives &lt;code&gt;isOpen&lt;/code&gt; as a prop. The state lives in the closest common ancestor, which is the standard lift-state-up pattern. Straightforward.&lt;/p&gt;




&lt;h2&gt;
  
  
  The custom hook trap
&lt;/h2&gt;

&lt;p&gt;The cart count was where things got more interesting.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CartIcon&lt;/code&gt; in the banner needs to display the total items the user added to the cart. &lt;code&gt;ProductList&lt;/code&gt; needs to refresh that total after adding an item. Two components, one shared value — the obvious move is a custom hook that fetches the total from the backend and exposes a &lt;code&gt;refresh&lt;/code&gt; function to re-fetch on demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useCartCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cartCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCartCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* fetch /api/cart/cart-count */&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cartCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you call this hook in both components, it looks clean. It doesn't work.&lt;/p&gt;

&lt;p&gt;The issue is obvious in hindsight: &lt;strong&gt;each hook call is an independent instance&lt;/strong&gt;. &lt;code&gt;CartIcon&lt;/code&gt; and &lt;code&gt;ProductList&lt;/code&gt; each had their own isolated &lt;code&gt;cartCount&lt;/code&gt; state. When &lt;code&gt;ProductList&lt;/code&gt; called &lt;code&gt;refresh()&lt;/code&gt;, it updated its own copy. &lt;code&gt;CartIcon&lt;/code&gt; never knew anything changed.&lt;/p&gt;

&lt;p&gt;The hook shared the logic. Not the state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Context as the fix
&lt;/h2&gt;

&lt;p&gt;The solution is to own the state in a single Context provider and have both components consume from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CartContextProvider
├── Banner
│   └── CartIcon  ← reads cartCount
└── Home
    └── ProductList  ← calls refresh()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The custom hook still exists — but now it runs once inside &lt;code&gt;CartContextProvider&lt;/code&gt;. Everything downstream reads from that one instance via &lt;code&gt;useContext&lt;/code&gt;. When &lt;code&gt;ProductList&lt;/code&gt; calls &lt;code&gt;refresh()&lt;/code&gt;, it's the same function &lt;code&gt;CartIcon&lt;/code&gt; is subscribed to.&lt;/p&gt;

&lt;p&gt;The same pattern applied to auth. &lt;code&gt;Navigation&lt;/code&gt; needs &lt;code&gt;isLoggedIn&lt;/code&gt; to render the right links. &lt;code&gt;CartIcon&lt;/code&gt; needs it to decide whether to render at all. Pulling both into an &lt;code&gt;AuthContext&lt;/code&gt; replaced the old &lt;code&gt;showHideMenuItems()&lt;/code&gt; with declarative rendering — components just read state and React handles the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where login logic lives — and why it matters
&lt;/h2&gt;

&lt;p&gt;The first version had the login fetch inside the &lt;code&gt;Login&lt;/code&gt; component. It sent the request, and on success, called &lt;code&gt;navigate('/')&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem: the &lt;code&gt;Login&lt;/code&gt; component had no connection to &lt;code&gt;AuthContext&lt;/code&gt;. After a successful login, &lt;code&gt;isLoggedIn&lt;/code&gt; in the context hadn't changed. The banner still showed "Log in". The app looked broken until you refreshed.&lt;/p&gt;

&lt;p&gt;The fix is to move &lt;code&gt;login()&lt;/code&gt; and &lt;code&gt;logout()&lt;/code&gt; out of the component and into &lt;code&gt;AuthContext&lt;/code&gt; — so they have direct access to &lt;code&gt;setUser&lt;/code&gt; and can update the shared state immediately on success.&lt;/p&gt;

&lt;p&gt;This also revealed a cleaner separation of concerns. &lt;code&gt;useAuth&lt;/code&gt; should only do one thing: check the session on mount and expose the user state. The actions — login, logout — belong in the context that owns that state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useAuth&lt;/code&gt;&lt;/strong&gt; — runs &lt;code&gt;checkAuth&lt;/code&gt; on mount, returns &lt;code&gt;{ user, setUser }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AuthContext&lt;/code&gt;&lt;/strong&gt; — defines &lt;code&gt;login()&lt;/code&gt; and &lt;code&gt;logout()&lt;/code&gt;, uses &lt;code&gt;setUser&lt;/code&gt; from the hook to update state, provides everything to the tree&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This settled into a clear rule: &lt;strong&gt;the hook manages state, the context owns actions, the component owns UI&lt;/strong&gt; — error messages, button disabled state, loading indicators. The &lt;code&gt;Login&lt;/code&gt; component now just calls &lt;code&gt;login()&lt;/code&gt; from context and reacts to the response.&lt;/p&gt;




&lt;h2&gt;
  
  
  Session cookies vs localStorage
&lt;/h2&gt;

&lt;p&gt;There are two common ways to handle auth persistence on the frontend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JWT + localStorage&lt;/strong&gt; — on login, the backend returns a token. The frontend stores it in &lt;code&gt;localStorage&lt;/code&gt; and manually attaches it to every request as an &lt;code&gt;Authorization&lt;/code&gt; header. It persists across refreshes, but the token is exposed to JavaScript, making it vulnerable to XSS attacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session cookies&lt;/strong&gt; — on login, the backend sets an &lt;code&gt;httpOnly&lt;/code&gt; cookie. The browser stores it and attaches it automatically on every subsequent request to the same origin. Because &lt;code&gt;httpOnly&lt;/code&gt; cookies are inaccessible to JavaScript, XSS can't steal them. The one thing you need on the frontend is &lt;code&gt;credentials: 'include'&lt;/code&gt; on your fetch calls — by default, &lt;code&gt;fetch&lt;/code&gt; does not send cookies. This option tells the browser to include them, so the backend can read the session and know who you are.&lt;/p&gt;

&lt;p&gt;This app uses session cookies. The consequence for React state is that &lt;code&gt;isLoggedIn&lt;/code&gt; doesn't survive a page refresh — it's just JavaScript in memory. On every mount, &lt;code&gt;useAuth&lt;/code&gt; calls &lt;code&gt;/api/auth/me&lt;/code&gt;. If the session cookie is still valid, the backend confirms the user and state is restored. If not, the user is logged out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React state reflects auth. The cookie owns it.&lt;/strong&gt; These are two separate things and keeping them separate is the right mental model.&lt;/p&gt;




&lt;h2&gt;
  
  
  The actual takeaway
&lt;/h2&gt;

&lt;p&gt;The migration made one thing very concrete: &lt;strong&gt;vanilla JS treats the DOM as the source of truth; React treats state as the source of truth&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the old app, &lt;code&gt;showHideMenuItems()&lt;/code&gt; existed because the DOM didn't know about login state — you had to reach in and update it manually every time something changed. In React, &lt;code&gt;isLoggedIn&lt;/code&gt; changes once and everything that depends on it re-renders. The DOM is a side effect of state, not the thing you manage directly.&lt;/p&gt;

&lt;p&gt;That shift — from imperative DOM manipulation to declarative state — is what every React pattern ultimately comes back to. Lifting state up, custom hooks, Context — each one is just a different answer to the same question: how far does this state need to reach?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
    </item>
  </channel>
</rss>
