{
    "version": "https://jsonfeed.org/version/1",
    "title": "Frontend Notes",
    "description": "",
    "home_page_url": "https://bondar-blog.pages.dev",
    "feed_url": "https://bondar-blog.pages.dev/feed.json",
    "user_comment": "",
    "author": {
        "name": "Anastasiia Bondar"
    },
    "items": [
        {
            "id": "https://bondar-blog.pages.dev/alpinestore-in-hyvae/",
            "url": "https://bondar-blog.pages.dev/alpinestore-in-hyvae/",
            "title": "Alpine.store in the Hyvä theme",
            "summary": "Introduction If you have worked with Hyvä, you are already familiar with Alpine.js and how it’s used within the theme. While the default Hyvä theme&hellip;",
            "content_html": "<h2 id=\"introduction\">Introduction</h2>\n<p>If you have worked with Hyvä, you are already familiar with Alpine.js and how it’s used within the theme. While the default Hyvä theme does not make use of <code>Alpine.store</code>, in real-world Hyvä-based projects it can be very useful in specific scenarios.</p><hr>\n<h3 id=\"what-is-alpinestore\">What Is Alpine.store?</h3>\n<p><code>Alpine.store</code> (available since Alpine.js v3) creates a <strong>reactive global data object</strong> that exists outside any component’s scope.</p><p>Unlike <code>x-data</code>, which is scoped to a specific DOM subtree, a store is:</p><ul>\n<li>Globally accessible</li>\n<li>Shared across all Alpine components</li>\n<li>Available via the <code>$store</code> magic property</li>\n</ul>\n<p>You initialise a store once, and every component on the page can read and write to it reactively.</p><hr>\n<h4 id=\"initialising-a-store\">Initialising a Store</h4>\n<pre><code class=\"language-js\">document.addEventListener(&#39;alpine:init&#39;, () =&gt; {\n   Alpine.store(&#39;private&#39;, {\n       isLogged: false\n   });\n});\n</code></pre>\n<hr>\n<h4 id=\"reading-and-writing-store-state\">Reading and Writing Store State</h4>\n<pre><code class=\"language-html\">&lt;div x-data&gt;\n   &lt;template x-if=&quot;$store.private.isLogged&quot;&gt;\n       &lt;span&gt;Welcome back!&lt;/span&gt;\n   &lt;/template&gt;\n&lt;/div&gt;\n&lt;div x-data&gt;\n   &lt;button @click=&quot;$store.private.isLogged = true&quot;&gt;\n       Log in\n   &lt;/button&gt;\n&lt;/div&gt;\n</code></pre>\n<hr>\n<h4 id=\"using-the-store-inside-component-logic\">Using the Store Inside Component Logic</h4>\n<pre><code class=\"language-js\">function initComponent() {\n   return {\n       init() {\n           if (this.$store.private.isLogged) {\n               console.log(&#39;User is logged in&#39;);\n           }\n       }\n   }\n}\n</code></pre>\n<hr>\n<h3 id=\"when-to-use-alpinestore\">When to Use Alpine.store</h3>\n<table>\n<thead>\n<tr>\n<th>✅ Good fit when:</th>\n<th>❌ Avoid using it when:</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>Multiple independent layout blocks share the same state</td>\n<td>State is local to one component</td>\n</tr>\n<tr>\n<td>You need a <strong>single source of truth</strong></td>\n<td>You already rely heavily on event-based patterns</td>\n</tr>\n<tr>\n<td>Customer/session data must be accessible globally</td>\n<td>The problem can be solved with simple <code>$dispatch</code></td>\n</tr>\n</tbody></table>\n<h2 id=\"how-reactivity-works-across-components\">How Reactivity Works Across Components</h2>\n<p>Alpine internally wraps the store in the same reactive proxy system used by <code>x-data</code>.</p><p>This means:</p><ul>\n<li>Any property change triggers updates everywhere it’s used</li>\n<li>Components do <strong>not</strong> need to be related in the DOM</li>\n<li>No custom events or listeners are required</li>\n</ul>\n<hr>\n<h2 id=\"architecture-example--promo-campaign\">Architecture Example — Promo Campaign</h2>\n<p>Let’s abstract and imagine that you don’t have any existing way to implement a promo campaign, so this is just a theoretical example of how <code>Alpine.store</code> can be used.\nThe feature spans four independent Hyvä layout blocks, all sharing one reactive store.\n<figure class=\"post__image\"><img loading=\"lazy\" src=\"https://bondar-blog.pages.dev/media/posts/1/ChatGPT-Image-Apr-23-2026-09_47_25-AM.png\" alt=\"Image description\" width=\"1536\" height=\"1024\" sizes=\"(min-width: 920px) 703px, (min-width: 700px) calc(82vw - 35px), calc(100vw - 81px)\" srcset=\"https://bondar-blog.pages.dev/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_25-AM-xs.png 300w ,https://bondar-blog.pages.dev/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_25-AM-sm.png 480w ,https://bondar-blog.pages.dev/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_25-AM-md.png 768w ,https://bondar-blog.pages.dev/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_25-AM-lg.png 1024w\"></figure></p><hr>\n<h3 id=\"data-flow\">Data Flow</h3>\n<figure class=\"post__image\"><img loading=\"lazy\" src=\"https://bondar-blog.pages.dev/media/posts/1/ChatGPT-Image-Apr-23-2026-09_47_21-AM.png\" alt=\"Image description\" width=\"1536\" height=\"1024\" sizes=\"(min-width: 920px) 703px, (min-width: 700px) calc(82vw - 35px), calc(100vw - 81px)\" srcset=\"https://bondar-blog.pages.dev/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_21-AM-xs.png 300w ,https://bondar-blog.pages.dev/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_21-AM-sm.png 480w ,https://bondar-blog.pages.dev/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_21-AM-md.png 768w ,https://bondar-blog.pages.dev/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_21-AM-lg.png 1024w\"></figure><hr>\n<h3 id=\"1-store-initialization\">1. Store Initialization</h3>\n<p>Register once in your theme’s <code>default_head_blocks.xml</code> or a dedicated JS module.\nThe store listens to <code>@update-totals.window</code> , the same event Hyvä’s own <code>initCartTotals()</code> uses and extracts the subtotal from <code>total_segments</code>.</p><p>Guard the registration so that a second call (from a duplicate layout block or a third-party module re-dispatching <code>alpine:init</code>) does not silently reset accumulated state:</p><pre><code class=\"language-php\">&lt;?php\n// gift-store-init.phtml\n/** @var \\Your\\Module\\Block\\PromoGifts $block */\n/** @var \\Magento\\Framework\\Escaper $escaper */\n/** @var \\Hyva\\Theme\\ViewModel\\HyvaCsp $hyvaCsp */\n?&gt;\n&lt;script&gt;\ndocument.addEventListener(&#39;alpine:init&#39;, () =&gt; {\n    if (Alpine.store(&#39;promo&#39;)) return; // already registered — do not overwrite\n\n    Alpine.store(&#39;promo&#39;, {\n\n        // ── Config ────────────────────────────────────────────────\n        threshold: 100,\n\n        // ── State ─────────────────────────────────────────────────\n        cartTotal: 0,\n        gifts: &lt;?= /** @noEscape */ json_encode($block-&gt;getPromoGifts()) ?&gt;,\n        selectedGift: null,\n        drawerOpen: false,\n        bannerVisible: true,\n\n        // ── Computed getters ──────────────────────────────────────\n        // These recalculate whenever the flat primitives they depend\n        // on (cartTotal, threshold) are reassigned. Avoid replacing\n        // primitives with nested objects — mutations to nested\n        // properties do not trigger getter recalculation.\n        get remaining() {\n            return Math.max(0, this.threshold - this.cartTotal);\n        },\n        get isEligible() {\n            return this.cartTotal &gt;= this.threshold;\n        },\n        get progress() {\n            return Math.min(100, (this.cartTotal / this.threshold) * 100);\n        },\n\n        // ── Actions ───────────────────────────────────────────────\n        selectGift(gift) {\n            this.selectedGift = gift;\n        },\n        openDrawer() {\n            this.drawerOpen = true;\n        },\n        closeDrawer() {\n            this.drawerOpen = false;\n        },\n        dismissBanner() {\n            this.bannerVisible = false;\n        },\n\n        // ── Cart sync ─────────────────────────────────────────────\n        // totalsData matches window.checkoutConfig.totalsData shape:\n        // { total_segments: [{ code: &#39;subtotal&#39;, value: 58.00 }, ...] }\n        syncCart(totalsData) {\n            const segment = totalsData?.total_segments\n                ?.find(s =&gt; s.code === &#39;subtotal&#39;);\n            this.cartTotal = segment?.value ?? 0;\n\n            if (this.isEligible &amp;&amp; !this.selectedGift) {\n                this.openDrawer();\n            }\n        },\n\n        // ── Event listeners ───────────────────────────────────────\n        // Mirrors the pattern used in Hyvä&#39;s own initCartTotals()\n        eventListeners: {\n            [&#39;@update-totals.window&#39;]($event) {\n                Alpine.store(&#39;promo&#39;).syncCart($event.detail.data);\n            }\n        }\n    });\n});\n&lt;/script&gt;\n&lt;?php $hyvaCsp-&gt;registerInlineScript() ?&gt;\n\n&lt;!-- Place once in default.xml, outside #maincontent --&gt;\n&lt;div x-data x-bind=&quot;$store.promo.eventListeners&quot;&gt;&lt;/div&gt;\n</code></pre>\n<p>Hyvä’s <code>hyva.replaceDomElement()</code> (used by <code>cart.phtml</code> after cart operations) replaces the main content area without re-firing <code>alpine:init</code>, so the store survives intact. Any listener element placed inside <code>#maincontent</code> would be destroyed and never re-attached.</p><hr>\n<h3 id=\"2-promo-banner--headerphtml\">2. Promo Banner — <code>header.phtml</code></h3>\n<pre><code class=\"language-html\">&lt;div\n    x-data\n    x-show=&quot;$store.promo.bannerVisible&quot;\n    class=&quot;promo-banner&quot;\n&gt;\n         &lt;!-- your code --&gt;\n&lt;/div&gt;\n</code></pre>\n<hr>\n<h3 id=\"3-promo-widget--cartphtml\">3. Promo Widget — <code>cart.phtml</code></h3>\n<pre><code class=\"language-html\">&lt;div x-data class=&quot;promo-widget&quot;&gt;\n    &lt;template x-if=&quot;!$store.promo.isEligible&quot;&gt;\n        &lt;!-- your code --&gt;\n    &lt;/template&gt;\n\n    &lt;template x-if=&quot;$store.promo.isEligible &amp;&amp; !$store.promo.selectedGift&quot;&gt;\n         &lt;!-- your code --&gt;\n    &lt;/template&gt;\n\n    &lt;template x-if=&quot;$store.promo.selectedGift&quot;&gt;\n       &lt;!-- your code --&gt;\n    &lt;/template&gt;\n&lt;/div&gt;\n</code></pre>\n<hr>\n<h3 id=\"4-gift-chooser-drawer--gift-drawerphtml\">4. Gift Chooser Drawer — <code>gift-drawer.phtml</code></h3>\n<pre><code class=\"language-html\">&lt;div\n   x-data\n   x-show=&quot;$store.promo.drawerOpen&quot;\n   @keydown.escape.window=&quot;$store.promo.closeDrawer()&quot;\n&gt;\n   &lt;div class=&quot;drawer-header&quot;&gt;\n       &lt;h2&gt;Choose your free gift&lt;/h2&gt;\n       &lt;button @click=&quot;$store.promo.closeDrawer()&quot;&gt;✕&lt;/button&gt;\n   &lt;/div&gt;\n   &lt;div class=&quot;gift-grid&quot;&gt;\n       &lt;template x-for=&quot;gift in $store.promo.gifts&quot; :key=&quot;gift.id&quot;&gt;\n         &lt;!-- your code --&gt;\n       &lt;/template&gt;\n    &lt;/div&gt;\n&lt;/div&gt;\n</code></pre>\n<hr>\n<h3 id=\"5-product-badge--product-itemphtml\">5. Product Badge — <code>product-item.phtml</code></h3>\n<pre><code class=\"language-html\">&lt;!-- Rendered inside each product card in the listing --&gt;\n&lt;div x-data &gt;\n   &lt;template x-if=&quot;!$store.promo.isEligible &amp;&amp; product.contributesToPromo&quot;&gt;\n       &lt;!-- your code --&gt;\n   &lt;/template&gt;\n&lt;/div&gt;\n</code></pre>\n<hr>\n<h2 id=\"store-lifetime-and-re-initialization\">Store Lifetime and Re-initialization</h2>\n<p>An <code>Alpine.store</code> is registered once and lives for the entire page session. In standard Magento/Hyvä full-page loads this is fine - the store is torn down with the page. Two edge cases are worth knowing.</p><p><strong>Double initialization.</strong> If <code>alpine:init</code> fires more than once, for example, a layout block is included in two places, or a third-party module re-dispatches the event calling <code>Alpine.store(&#39;promo&#39;, { ... })</code> a second time silently overwrites the first registration. Any state accumulated since first load (<code>cartTotal</code>, <code>selectedGift</code>, <code>drawerOpen</code>) is reset. The guard at the top of the registration block handles this:</p><pre><code class=\"language-javascript\">if (Alpine.store(&#39;promo&#39;)) return;\n</code></pre>\n<p><strong>Hyvä’s <code>replaceDomElement</code>.</strong> As seen in <code>cart.phtml</code>, Hyvä can swap out <code>#maincontent</code> via <code>hyva.replaceDomElement()</code> after cart operations. This re-renders the DOM but does <strong>not</strong> re-run <code>alpine:init</code>  the store survives intact, which is exactly what you want. This is why the cart-sync listener must live outside <code>#maincontent</code> in <code>default.xml</code>.</p><hr>\n<h2 id=\"summary\">Summary</h2>\n<p><code>Alpine.store</code> is a simple and powerful way to share reactive state across independent components.</p><p>For state that stays inside a single <code>.phtml</code>, keep using <code>x-data</code>. Reach for <code>Alpine.store</code> when the state genuinely needs to escape component boundaries.</p><p>Used correctly, it helps maintain a clean architecture and avoids unnecessary event complexity.</p><p>Further reading: <a href=\"https://alpinejs.dev/magics/store\">Alpine.js $store documentation</a>    ·   <a href=\"https://alpinejs.dev/globals/alpine-store\">Alpine.js Alpine.store() API  documentation</a></p>",
            "image": "https://bondar-blog.pages.dev/media/posts/1/Screenshot-2026-04-23-at-13.29.04.png",
            "author": {
                "name": "Anastasiia Bondar"
            },
            "tags": [
                   "Magento2",
                   "Hyvä",
                   "Alpine"
            ],
            "date_published": "2026-04-22T12:46:30+01:00",
            "date_modified": "2026-04-23T14:33:11+01:00"
        }
    ]
}
