{
    "version": "https://jsonfeed.org/version/1",
    "title": "Frontend Notes",
    "description": "",
    "home_page_url": "https://bondar.blog",
    "feed_url": "https://bondar.blog/feed.json",
    "user_comment": "",
    "author": {
        "name": "Anastasiia Bondar"
    },
    "items": [
        {
            "id": "https://bondar.blog/alpinestore-in-hyva/",
            "url": "https://bondar.blog/alpinestore-in-hyva/",
            "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 <a href=\"https://www.hyva.io/\">Hyvä</a>, you are already familiar with Alpine.js and how it’s used within the theme. While the <a href=\"https://github.com/hyva-themes/magento2-default-theme\">default Hyvä theme</a> 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 <a href=\"https://alpinejs.dev/\">Alpine.js v3</a>) 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 initialize a store once, and every component on the page can read and update it reactively.</p><hr>\n<h4 id=\"initializing-a-store\">Initializing a Store</h4>\n<pre><code class=\"language-js\">document.addEventListener(&#39;alpine:init&#39;, () =&gt; {\n   Alpine.store(&#39;private&#39;, {\n       isClicked: 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.isClicked&quot;&gt;\n       &lt;span&gt;You have clicked `Click Me` before &lt;/span&gt;\n   &lt;/template&gt;\n&lt;/div&gt;\n&lt;div x-data&gt;\n   &lt;button @click=&quot;$store.private.isClicked = true&quot;&gt;\n       Click Me\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.isClicked) {\n               console.log(&#39;Component is initialized after the click&#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 wraps stores in its reactive system (the same mechanism as Vue’s reactivity engine). This ensures that updates to store properties automatically propagate to any component that depends on them.</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 to keep Alpine components in sync with each other</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. This is a simplified, theoretical example of how <code>Alpine.store</code> can be used.</p><p>The feature spans four independent Hyvä layout blocks, all sharing one reactive store.</p><figure class=\"post__image\"><img loading=\"lazy\" src=\"https://bondar.blog/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/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_25-AM-xs.png 300w ,https://bondar.blog/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_25-AM-sm.png 480w ,https://bondar.blog/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_25-AM-md.png 768w ,https://bondar.blog/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_25-AM-lg.png 1024w\"></figure><hr>\n<h3 id=\"data-flow\">Data Flow</h3>\n<figure class=\"post__image\"><img loading=\"lazy\" src=\"https://bondar.blog/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/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_21-AM-xs.png 300w ,https://bondar.blog/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_21-AM-sm.png 480w ,https://bondar.blog/media/posts/1/responsive/ChatGPT-Image-Apr-23-2026-09_47_21-AM-md.png 768w ,https://bondar.blog/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 the store once, either 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.\n                // Keep store shape simple when possible.\n                // Deeply nested state is still reactive, \n                // but flatter structures are usually easier to reason about and maintain.\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                // for demo purposes it only handles the transition into eligibility\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 lifetime of the current page. 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 and calls <code>Alpine.store(&#39;promo&#39;, { ... })</code> again, re-registering a store with the same name. This can overwrite its existing state. While this behavior is not emphasized in the Alpine documentation, guarding the initialization is a good defensive practice to prevent accidental resets caused by duplicate layout blocks or re-dispatched <code>alpine:init</code> events.</p><p>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>, so 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 reduces the need for cross-component event wiring.</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/media/posts/1/HyvaAlpineStore.jpg",
            "author": {
                "name": "Anastasiia Bondar"
            },
            "tags": [
                   "Magento2",
                   "Hyvä",
                   "Alpine"
            ],
            "date_published": "2026-04-23T15:18:00+01:00",
            "date_modified": "2026-04-24T13:50:06+01:00"
        }
    ]
}
