WildflowerJS Reactive JS, No BS*

A no-build reactive JavaScript framework, rooted in the web platform.
No build step. No dependencies. No lock-in.

<script src="wildflower.min.js"></script> ...and start building.

Back to Basics

The code you write is 100% web standard code. HTML stays HTML. JavaScript stays JavaScript. CSS stays CSS. No JSX, no templating language, no custom syntax to learn. If you know the web platform, you already know how to use this.

WildflowerJS extends the web platform. It doesn't replace it.

Your Development Simplified

Because you develop with 100% web standards, every tool in your existing chain already understands the code: IDE, browser DevTools, linter, formatter, screen reader, SEO crawler. Nothing to install, no custom file types, no sourcemaps. Save the file, refresh, and your change is live.

Just be a web developer.

Batteries Included: One Mental Model

Router, SSR, stores, computed properties, two-way binding, event modifiers, data pools, and TypeScript types, all built in, all speaking the same language. Learn data-bind once and you know binding everywhere: lists, pools, stores, forms. There's no five-library stack to keep in sync.

One script tag. Everything you need.

<div data-component="counter">
  <span data-bind="count"></span>
  <button data-action="increment">
    +1
  </button>
</div>

<script>
wildflower.component('counter', {
  state: { count: 0 },
  increment() { this.count++ }
})
</script>

How It Works

data-bind connects state to the DOM.

data-action connects events to methods.

this.count++ triggers a precise DOM update.

Mutate state. The DOM updates.

Two Reactivity Modes

data-list for automatic reactivity: mutate state, DOM updates. data-pool for explicit control: plain objects, zero proxy overhead, you say what changed.

Same template syntax. Different performance profile. From interactive forms to per-frame particle systems. You choose the right tradeoff for the job.

Try it. Right-click, inspect this demo. Every dot is a real DOM element.

See full demo →

* Build Step

Zero Toolchain

Modern frameworks ask you to install a compiler, a bundler, a package manager, hundreds of fragile transitive dependencies, and a framework-specific file format, before you write a single line of your application.

WildflowerJS was built starting from a single principle: no build step, no tooling. Ever.

WildflowerJS asks you to add a script tag.

There's no CLI scaffolding step, no config files, no .vue/.jsx/.svelte source format. You don't debug through sourcemaps or wait on a build pipeline. Your project has zero dependencies.

Performance isn't a tradeoff. Build steps optimize bundle delivery, not the runtime work that follows it. WildflowerJS writes directly to the DOM, with no virtual DOM or reconciliation pass between state change and update, so it doesn't need a build step to be fast.

The framework is full-featured without the toolchain: router, SSR, stores, computed properties, transitions, pools. You don't need a toolchain to use any of it.

my-app/
  index.html
  app.js
  style.css
  wildflower.min.js

That's the entire project. No package.json.
No node_modules. No config files. Ship it.

Zero Install. Zero Attack Surface.

Every dependency you install is trust extended to a maintainer you've never met, running scripts on your dev machine and in your CI. A typical React + Vite + UI‑lib setup pulls in 300+ transitive packages before you write a feature.

Each one is a potential intrusion vector. NPM worms, OAuth chains compromising deploy platforms, postinstall hijacking: the supply chain is now where production code gets compromised, not the deploy. And signing isn't a backstop: Mini Shai‑Hulud (May 2026) compromised 170+ packages whose malicious versions carried valid SLSA Build Level 3 provenance, because the attestation came from build infrastructure the worm had already taken over.

WildflowerJS users don't have this attack surface, by construction. There is no npm install, no postinstall script, no transitive package graph. The framework is one file you copy or pin by hash.

As of v1.1, the same holds for building the framework itself. WildflowerJS bundles with a vendored rollup and terser pipeline pulled as three SHA‑512‑pinned tarballs: no npm install, no transitive packages, no postinstall scripts in the build path. The entire toolchain is three files you verify by hash.

Zero dependencies is the absence of a problem the rest of the industry has not properly addressed.

A typical React/Vue project:

  npm install
  ├── hundreds of packages
  ├── from hundreds of maintainers
  ├── postinstall scripts run on install
  └── tens to hundreds of MB of transitive code

WildflowerJS:

  <script src="wildflower.min.js"></script>
  └── 1 file.
      No transitive dependencies.

Zero Lock-in

WildflowerJS works with the DOM, not instead of it. There's no virtual DOM intercepting your code and no compiler rewriting your markup. The render cycle is yours.

That means Leaflet, DataTables, Chart.js, D3, Three.js, any library that touches the DOM, just works. No wrapper packages or framework-specific escape hatches required. Drop in a script tag and use it.

Because your code is standard HTML and JavaScript, you're never locked in. Your skills transfer and your code is more portable. If you outgrow the framework, your knowledge doesn't expire.

This also means your "ecosystem" is all of the world of vanilla JS. Without compromises or hacks.

<!-- Use any library directly -->
<div data-component="map-view">
  <div id="map" style="height: 400px"></div>
</div>
wildflower.component('map-view', {
  state: { lat: 51.505, lng: -0.09 },
  init() {
    // Leaflet works as-is. No wrappers.
    this._map = L.map('map')
      .setView([this.lat, this.lng], 13);
    L.tileLayer('https://{s}.tile.osm.org'
      + '/{z}/{x}/{y}.png').addTo(this._map);
  }
})

Precise Reactivity

When you write this.count++, WildflowerJS updates the single DOM node bound to count. Nothing else is touched. There's no tree diffing or reconciliation pass to figure that out.

This isn't a tradeoff. You get fine-grained updates and a simple mental model. Change a property, the bound element updates. That's the entire reactivity model.

Other frameworks ask you to learn signals, accessors, memos, effects, and subscription lifecycles to achieve what WildflowerJS does with a property assignment.

wildflower.component('dashboard', {
  state: {
    users: 1420,
    status: 'healthy'
  },
  computed: {
    summary() {
      return this.users + ' users, ' + this.status;
    }
  },
  refresh() {
    this.users = 1421;
    // Only the elements bound to 'users'
    // and 'summary' update. Everything
    // else on the page is untouched.
  }
})

One Reactivity Model. Everywhere.

Components, Stores, and Plugins all share the same reactive foundation. State, computed properties, and methods work identically no matter where they live. Learn it once, it works the same way in a UI component, a global store, or a framework plugin.

Other frameworks make you learn a different system for each layer. React components use hooks, but stores need Redux or Zustand, which are completely different APIs. Vue components use reactive data, but Pinia stores have their own patterns. Every layer is a new mental model.

In WildflowerJS, there's one model. A store is a component without a template. A plugin is an entity that extends the framework itself, adding directives, lifecycle hooks, and services. The same this.count++ triggers the same reactivity everywhere.

This unlocks patterns other frameworks can't express. A store can run headless physics simulations with tick(), feeding data into a component that renders it through a pool, all using the same reactive primitives, no glue code required.

// Component: reactive UI
wildflower.component('cart', {
  state: { items: [] },
  computed: {
    total() { return this.items.length; }
  }
})

// Store: global shared state
wildflower.store('user', {
  state: { name: '', role: 'guest' },
  computed: {
    isAdmin() { return this.role === 'admin'; }
  }
})

// Plugin: extends the framework
wildflower.plugin({
  name: 'notifications',
  state: { items: [], unreadCount: 0 },
  computed: {
    hasUnread() { return this.unreadCount > 0; }
  },
  add(msg) { this.items.push(msg); this.unreadCount++; }
})
// Access globally: wildflower.$notifications.add(...)

// Same state. Same computed. Same methods.

Data Pools

Every framework wraps collection items in reactive proxies, whether the item needs it or not. WildflowerJS gives you a choice: data-list for push reactivity (automatic), data-pool for pull reactivity (explicit control, zero proxy overhead).

Pools render plain objects with the same template syntax as lists. Mutate the object, call markDirty(), and only that item updates. Full CRUD, selection, bulk operations, all faster than the push-reactive path.

And because pools use pull-based rendering, they scale to simulations, games, particle systems, and data visualizations at native frame rate. Use cases that would choke a virtual DOM. No other framework has anything like this.

<div data-component="user-table">
  <tbody data-pool="users" data-key="id">
    <template>
      <tr>
        <td data-bind="name"></td>
        <td data-bind="status"
            data-bind-class="status === 'active'
              ? 'badge success'
              : 'badge inactive'"></td>
      </tr>
    </template>
  </tbody>
</div>
wildflower.component('user-table', {
  pools: { users: {} },

  init() {
    // Populate: plain objects, no proxies
    data.forEach(u => this.pools.users.add(u));
  },

  // Optional: add tick() and the same pool
  // renders every frame. Same template, same
  // data, different rendering frequency.
  // That's the only difference between a
  // display table and a particle system.
})

Built for AI-Assisted Development

Because WildflowerJS is standard HTML and JavaScript, AI code assistants already know how to write it. There's no custom syntax to hallucinate or compiler quirks to work around. The code an AI generates runs exactly as written, with no build step between generation and execution.

We go further. WildflowerJS ships an AI-optimized reference page with patterns, anti-patterns, and examples designed for code generation context windows. Our llms.txt file follows the llms.txt convention for machine-readable documentation.

And for structured app generation, our Universal App Manifest lets you describe an entire application as a JSON schema (components, state, computed properties, methods, templates) and have an AI generate the working code from the manifest, mediated through framework-specific idiom files.

You: "Build me a todo app with
WildflowerJS"

AI reads llms.txt or ai-assistant.html
     ↓
Generates standard HTML + JS
     ↓
<div data-component="todo-app">
  <input data-model="newItem">
  <button data-action="addItem">
    Add
  </button>
  <ul data-list="items">
    <template>
      <li data-bind="text"></li>
    </template>
  </ul>
</div>
     ↓
Open in your browser. It works, and you can read and understand the code.

Advanced Store Patterns

Deep dive into advanced store techniques including store-to-store communication, persistence, lifecycle management, and comprehensive API reference.

📚 Prerequisites: This page covers advanced store patterns. If you're new to stores, start with the Basic Stores first.

Store-to-Store Communication

Stores can communicate with each other for complex state management, enabling sophisticated workflows and real-time updates:

// Shopping Cart Store - Central e-commerce state management
wildflower.store('cart', {
    state: {
        items: [],
        discount: 0,
        promoCode: '',
        shipping: 0,
        tax: 0,
        taxRate: 0.08
    },

    computed: {
        itemCount() {
            return this.items.reduce((sum, item) => sum + item.quantity, 0)
        },

        subtotal() {
            return this.items.reduce((sum, item) =>
                sum + (item.price * item.quantity), 0
            )
        },

        // Formatted versions for display
        subtotalFormatted() {
            return this.subtotal.toFixed(2)
        },

        taxFormatted() {
            return this.tax.toFixed(2)
        },

        total() {
            return Math.max(0, this.subtotal - this.discount + this.shipping + this.tax).toFixed(2)
        },

        isEmpty() {
            return this.items.length === 0
        }
    },

    // Methods (unified paradigm - at top level)
    addItem(product) {
        const existingItem = this.items.find(item => item.productId === product.id)

        if (existingItem) {
            existingItem.quantity += 1
        } else {
            this.items.push({
                id: Date.now(),
                productId: product.id,
                name: product.name,
                price: product.price,
                quantity: 1,
                addedAt: new Date()
            })
        }

        // Notify analytics store
        const analyticsStore = wildflower.getStore('analytics')
        analyticsStore.trackEvent('item_added', {
            productId: product.id,
            productName: product.name,
            price: product.price,
            cartTotal: this.subtotal
        })

        // Check for promotional discounts
        this.updatePromotions()
    },

    removeItem(itemId) {
        const item = this.items.find(item => item.id === itemId)
        if (item) {
            // Track removal
            const analyticsStore = wildflower.getStore('analytics')
            analyticsStore.trackEvent('item_removed', {
                productId: item.productId,
                productName: item.name,
                price: item.price,
                quantity: item.quantity
            })

            // Remove from cart
            const index = this.items.findIndex(item => item.id === itemId)
            this.items.splice(index, 1)

            // Recalculate promotions
            this.updatePromotions()
        }
    },

    updateQuantity(itemId, quantity) {
        const item = this.items.find(item => item.id === itemId)
        if (item && quantity > 0) {
            item.quantity = quantity
            this.updatePromotions()
        }
    },

    applyPromoCode(code) {
        this.promoCode = code
        this.updatePromotions()
    },

    updatePromotions() {
        const promoStore = wildflower.getStore('promotions')
        const discount = promoStore.calculateDiscount(this.items, this.promoCode)
        this.discount = discount

        // Calculate shipping
        this.shipping = this.subtotal >= 50 ? 0 : 5.99

        // Calculate tax
        this.tax = (this.subtotal - this.discount) * this.taxRate
    },

    clearCart() {
        const analyticsStore = wildflower.getStore('analytics')
        analyticsStore.trackEvent('cart_cleared', {
            itemCount: this.items.length,
            totalValue: this.subtotal
        })

        this.items = []
        this.discount = 0
        this.promoCode = ''
        this.shipping = 0
        this.tax = 0
    }
})
// Analytics Store - Track user behavior and generate insights
wildflower.store('analytics', {
    state: {
        events: [],
        sessionId: null,
        startTime: null,
        pageViews: 0,
        conversionFunnel: {
            views: 0,
            addToCart: 0,
            checkout: 0,
            purchase: 0
        }
    },

    computed: {
        sessionDuration() {
            return Math.round((new Date() - this.startTime) / 1000)
        },

        conversionRate() {
            return this.conversionFunnel.views > 0
                ? Math.round((this.conversionFunnel.addToCart / this.conversionFunnel.views) * 100)
                : 0
        },

        recentEvents() {
            return this.events.slice(-10).reverse()
        }
    },

    init() {
        this.sessionId = 'session_' + Date.now()
        this.startTime = new Date()
        this.trackEvent('session_start', {
            timestamp: this.startTime,
            userAgent: navigator.userAgent
        })
    },

    // Methods (unified paradigm - at top level)
    trackEvent(eventName, data) {
        const event = {
            id: Date.now(),
            event: eventName,
            data: data,
            formattedTime: new Date().toLocaleTimeString(),
            sessionId: this.sessionId
        }

        this.events.push(event)

        // Update conversion funnel
        this.updateConversionFunnel(eventName, data)

        // Send to analytics service (simulated)
        this.sendToAnalytics(event)

        // Keep events list manageable
        if (this.events.length > 100) {
            this.events = this.events.slice(-50)
        }
    },

    updateConversionFunnel(eventName, data) {
        switch (eventName) {
            case 'item_added':
                this.conversionFunnel.addToCart++
                break
            case 'checkout_started':
                this.conversionFunnel.checkout++
                break
            case 'purchase_completed':
                this.conversionFunnel.purchase++
                break
        }
    },

    trackPageView(page) {
        this.pageViews++
        this.conversionFunnel.views++
        this.trackEvent('page_view', {
            page: page,
            totalViews: this.pageViews
        })
    },

    async sendToAnalytics(event) {
        try {
            // Simulate API call
            console.log('📊 Analytics Event:', event)

            // In a real app, you'd send to your analytics service
            // await fetch('/api/analytics', {
            //     method: 'POST',
            //     headers: { 'Content-Type': 'application/json' },
            //     body: JSON.stringify(event)
            // })
        } catch (error) {
            console.error('Analytics error:', error)
        }
    },

    clearEvents() {
        this.events = []
    }
})
// Promotions Store - Handle discounts and special offers
wildflower.store('promotions', {
    state: {
        availablePromotions: [
            { code: 'SAVE10', discount: 0.10, minAmount: 25, description: '10% off orders over $25' },
            { code: 'WELCOME', discount: 5, minAmount: 0, description: '$5 off first order' },
            { code: 'BULK20', discount: 0.20, minAmount: 100, description: '20% off orders over $100' }
        ],
        activePromotions: [],
        seasonalMultiplier: 1.0
    },
    
    computed: {
        activePromoCodes() {
            return this.availablePromotions.map(p => p.code)
        }
    },

    // Methods at top level (Unified Entity Paradigm)
    calculateDiscount(cartItems, promoCode) {
        const subtotal = cartItems.reduce((sum, item) =>
            sum + (item.price * item.quantity), 0
        )

        let discount = 0

        // Apply promo code discount
        if (promoCode) {
            const promo = this.availablePromotions.find(p => p.code === promoCode)
            if (promo && subtotal >= promo.minAmount) {
                if (promo.discount < 1) {
                    // Percentage discount
                    discount += subtotal * promo.discount
                } else {
                    // Fixed amount discount
                    discount += promo.discount
                }
            }
        }

        // Apply automatic quantity discounts
        const itemCount = cartItems.reduce((sum, item) => sum + item.quantity, 0)
        if (itemCount >= 5) {
            discount += subtotal * 0.05 // 5% bulk discount
        }

        // Apply seasonal multiplier
        discount *= this.seasonalMultiplier

        // Track promotion usage
        if (discount > 0) {
            const analyticsStore = wildflower.getStore('analytics')
            analyticsStore.trackEvent('promotion_applied', {
                promoCode: promoCode,
                discount: discount,
                subtotal: subtotal,
                itemCount: itemCount
            })
        }

        return Math.round(discount * 100) / 100
    },

    validatePromoCode(code) {
        const promo = this.availablePromotions.find(p => p.code === code)
        return promo ? promo : null
    },

    addPromotion(promotion) {
        this.availablePromotions.push(promotion)
    },

    setSeasonalMultiplier(multiplier) {
        this.seasonalMultiplier = multiplier

        // Notify about seasonal changes
        const analyticsStore = wildflower.getStore('analytics')
        analyticsStore.trackEvent('seasonal_promotion_updated', {
            multiplier: multiplier
        })
    }
})
<div data-component="store-communication-demo">
    <p class="text-muted">Demonstrates how stores communicate and share data in real-time.</p>

    <div class="row">
        <div class="col-md-6">
            <!-- Shopping Cart -->
            <div class="card mb-3">
                <div class="card-header d-flex justify-content-between">
                    <h6 class="mb-0">Shopping Cart</h6>
                    <span class="badge bg-primary" data-bind="$cart.itemCount">0</span>
                </div>
                <div class="card-body">
                    <!-- Product Selection -->
                    <div class="mb-3">
                        <h6>Add Products:</h6>
                        <div class="d-grid gap-2">
                            <button data-action="addWidget" class="btn btn-primary btn-sm">
                                Add Widget ($19.99)
                            </button>
                            <button data-action="addGadget" class="btn btn-primary btn-sm">
                                Add Gadget ($29.99)
                            </button>
                            <button data-action="addTool" class="btn btn-primary btn-sm">
                                Add Tool ($39.99)
                            </button>
                        </div>
                    </div>
                    
                    <!-- Cart Items -->
                    <div data-show="!$cart.isEmpty">
                        <div class="mb-3">
                            <h6>Cart Items:</h6>
                            <div data-list="$cart.items">
                                <template>
                                    <div class="d-flex justify-content-between align-items-center py-2 border-bottom">
                                        <div>
                                            <strong data-bind="name"></strong>
                                            <br>
                                            <small class="text-muted">
                                                $<span data-bind="price"></span> x <span data-bind="quantity"></span>
                                            </small>
                                        </div>
                                        <button data-action="removeFromCart" class="btn btn-sm btn-danger">
                                            Remove
                                        </button>
                                    </div>
                                </template>
                            </div>
                        </div>
                        
                        <!-- Promo Code -->
                        <div class="mb-3">
                            <label class="form-label">Promo Code:</label>
                            <div class="d-flex gap-2">
                                <input type="text" data-model="promoCode" class="form-control"
                                       placeholder="Enter code (try SAVE10)">
                                <button data-action="applyPromo" class="btn btn-secondary">Apply</button>
                            </div>
                            <small class="text-muted">Try: SAVE10, WELCOME, BULK20</small>
                        </div>
                        
                        <!-- Cart Summary -->
                        <div class="border-top pt-3">
                            <div class="d-flex justify-content-between">
                                <span>Subtotal:</span>
                                <span>$<span data-bind="$cart.subtotalFormatted"></span></span>
                            </div>
                            <div class="d-flex justify-content-between" data-show="$cart.discount > 0">
                                <span class="text-success">Discount:</span>
                                <span class="text-success">-$<span data-bind="$cart.discount"></span></span>
                            </div>
                            <div class="d-flex justify-content-between">
                                <span>Shipping:</span>
                                <span>$<span data-bind="$cart.shipping"></span></span>
                            </div>
                            <div class="d-flex justify-content-between">
                                <span>Tax:</span>
                                <span>$<span data-bind="$cart.taxFormatted"></span></span>
                            </div>
                            <div class="d-flex justify-content-between fw-bold border-top pt-2">
                                <span>Total:</span>
                                <span>$<span data-bind="$cart.total"></span></span>
                            </div>
                        </div>
                        
                        <button data-action="clearCart" class="btn btn-danger mt-3 w-100">
                            Clear Cart
                        </button>
                    </div>
                    
                    <div data-show="$cart.isEmpty" class="text-center text-muted">
                        Cart is empty
                    </div>
                </div>
            </div>
        </div>
        
        <div class="col-md-6">
            <!-- Analytics Dashboard -->
            <div class="card mb-3">
                <div class="card-header">
                    <h6 class="mb-0">Analytics Dashboard</h6>
                </div>
                <div class="card-body">
                    <div class="row text-center mb-3">
                        <div class="col-6">
                            <div class="fw-bold" data-bind="$analytics.sessionDuration">0</div>
                            <small class="text-muted">Session (seconds)</small>
                        </div>
                        <div class="col-6">
                            <div class="fw-bold" data-bind="$analytics.conversionRate">0</div>
                            <small class="text-muted">Conversion %</small>
                        </div>
                    </div>
                    
                    <div class="mb-3">
                        <h6>Conversion Funnel:</h6>
                        <div class="small">
                            <div>Views: <span data-bind="$analytics.conversionFunnel.views"></span></div>
                            <div>Add to Cart: <span data-bind="$analytics.conversionFunnel.addToCart"></span></div>
                            <div>Checkout: <span data-bind="$analytics.conversionFunnel.checkout"></span></div>
                            <div>Purchase: <span data-bind="$analytics.conversionFunnel.purchase"></span></div>
                        </div>
                    </div>
                    
                    <div>
                        <h6>Recent Events:</h6>
                        <div style="max-height: 200px; overflow-y: auto;">
                            <div data-list="$analytics.recentEvents">
                                <template>
                                    <div class="small border-bottom py-1">
                                        <div class="d-flex justify-content-between gap-2">
                                            <span data-bind="event" class="fw-bold"></span>
                                            <span data-bind="formattedTime" class="text-muted text-nowrap"></span>
                                        </div>
                                    </div>
                                </template>
                            </div>
                        </div>
                    </div>
                    
                    <button data-action="clearAnalytics" class="btn btn-secondary btn-sm mt-3">
                        Clear Events
                    </button>
                </div>
            </div>
        </div>
    </div>
</div>
wildflower.component('store-communication-demo', {
    state: {
        promoCode: ''
    },

    // Subscribe to stores for this.stores auto-injection
    subscribe: {
        cart: ['items', 'discount'],
        analytics: ['events']
    },

    init() {
        // Initialize page view tracking using this.stores
        this.stores.analytics.trackPageView('store-communication-demo')
    },

    // Product addition actions
    addWidget() {
        this.stores.cart.addItem({
            id: 'widget',
            name: 'Widget',
            price: 19.99
        })
    },

    addGadget() {
        this.stores.cart.addItem({
            id: 'gadget',
            name: 'Gadget',
            price: 29.99
        })
    },

    addTool() {
        this.stores.cart.addItem({
            id: 'tool',
            name: 'Tool',
            price: 39.99
        })
    },

    // Cart management actions
    removeFromCart(event, element, details) {
        const itemId = details.item.id
        this.stores.cart.removeItem(itemId)
    },

    applyPromo() {
        this.stores.cart.applyPromoCode(this.promoCode.toUpperCase())
        this.promoCode = '' // Clear input
    },

    clearCart() {
        this.stores.cart.clearCart()
    },

    // Analytics actions
    clearAnalytics() {
        this.stores.analytics.clearEvents()
    }
})
Live Preview

Built-in Persistence (storageKey & autoSave)

WildflowerJS provides built-in localStorage persistence for stores. Simply add storageKey and autoSave options to your store definition:

// Automatic persistence - no manual localStorage code needed!
wildflower.store('app-settings', {
    storageKey: 'my-app-settings',  // localStorage key
    autoSave: true,                  // Auto-save on ANY state change

    state: {
        theme: 'light',
        fontSize: 'medium',
        notifications: true,
        recentItems: []
    },

    setTheme(theme) {
        this.theme = theme;
        // Automatically saved to localStorage!
    },

    addRecentItem(item) {
        this.recentItems = [...this.recentItems, item].slice(-10);
        // Nested changes are also auto-saved!
    }
});

// On page reload, state is automatically restored from localStorage
💡 Key Points:
  • storageKey - The localStorage key where state will be saved
  • autoSave: true - Automatically saves state on ANY change, including nested properties
  • State is automatically restored from localStorage when the store is created
  • Works with all state types: primitives, arrays, nested objects

When to use built-in persistence vs manual:

  • Built-in - Simple use cases where you want the entire store state persisted automatically
  • Manual - When you need selective persistence, encryption, or complex storage logic (see below)

Manual Store Persistence

For more control, implement custom persistence with lifecycle hooks. This demo saves your name and theme to localStorage. Change them, then refresh the page to see them persist:

// Preferences store: loads from localStorage on init, saves on change
wildflower.store('prefs', {
    state: { name: 'Guest', theme: 'light' },

    init() {
        const saved = localStorage.getItem('prefs');
        if (saved) {
            try {
                const data = JSON.parse(saved);
                if (data.name) this.name = data.name;
                if (data.theme) this.theme = data.theme;
            } catch (e) { /* ignore bad data */ }
        }
    },

    setName(name) {
        this.name = name;
        this._save();
    },

    setTheme(theme) {
        this.theme = theme;
        this._save();
    },

    _save() {
        localStorage.setItem('prefs', JSON.stringify({
            name: this.name, theme: this.theme
        }));
    },

    clearAll() {
        this.name = 'Guest';
        this.theme = 'light';
        localStorage.removeItem('prefs');
    }
});

Components bind to the store with $prefs.name and $prefs.theme. Changes persist across page reloads. The store's init() restores from localStorage on startup.

Store Lifecycle

Stores support full component lifecycle methods for initialization and cleanup, enabling sophisticated background operations and resource management:

// Data Service Store - Advanced lifecycle management with connections and retries
wildflower.store('data-service', {
    state: {
        connected: false,
        connecting: false,
        data: null,
        error: null,
        retryCount: 0,
        maxRetries: 5,
        connectionHistory: [],
        lastSync: null,
        syncStatus: 'idle', // idle, syncing, success, error
        connectionQuality: 100, // 0-100 percentage
        bandwidth: 'high', // low, medium, high
        latency: 0
    },
    
    computed: {
        connectionStatus() {
            if (this.connecting) return 'connecting'
            if (this.connected) return 'connected'
            if (this.error) return 'error'
            return 'disconnected'
        },

        qualityLevel() {
            if (this.connectionQuality >= 80) return 'excellent'
            if (this.connectionQuality >= 60) return 'good'
            if (this.connectionQuality >= 40) return 'fair'
            return 'poor'
        },

        recentHistory() {
            return this.connectionHistory.slice(0, 10)
        },

        dataAge() {
            if (!this.lastSync) return 0
            return Math.round((new Date() - this.lastSync) / 1000)
        }
    },

    // Initialize when store is created
    init() {
        console.log('🚀 Data Service Store initializing...')
        this.initializeConnectionHistory()
        this.connect()
        this.setupPeriodicSync()
        this.setupConnectionMonitoring()
    },

    // Cleanup when store is destroyed
    destroy() {
        console.log('🛑 Data Service Store destroying...')

        if (this.syncInterval) {
            clearInterval(this.syncInterval)
        }

        if (this.monitorInterval) {
            clearInterval(this.monitorInterval)
        }

        this.disconnect()
        this.logConnection('destroyed', 'Store destroyed')
    },

    // Helper methods
    delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
    },

    // Methods at top level (Unified Entity Paradigm)
    initializeConnectionHistory() {
        this.connectionHistory = []
        this.logConnection('initialization', 'Store initialized')
    },

    async connect() {
        if (this.connecting || this.connected) return

        try {
            this.connecting = true
            this.error = null
            this.logConnection('connecting', 'Attempting connection...')

            // Simulate connection negotiation
            await this.delay(Math.random() * 1000 + 500)

            // Simulate occasional connection failures
            if (Math.random() < 0.15) {
                throw new Error('Connection timeout')
            }

            this.connected = true
            this.connecting = false
            this.retryCount = 0
            this.connectionQuality = Math.round(Math.random() * 30 + 70)
            this.bandwidth = ['low', 'medium', 'high'][Math.floor(Math.random() * 3)]
            this.latency = Math.round(Math.random() * 100 + 20)

            this.logConnection('connected', 'Connection established successfully')
            this.loadInitialData()

        } catch (error) {
            this.connected = false
            this.connecting = false
            this.error = error.message
            this.retryCount++

            this.logConnection('error', `Connection failed: ${error.message}`)

            // Retry with exponential backoff
            if (this.retryCount < this.maxRetries) {
                const delay = Math.pow(2, this.retryCount) * 1000
                this.logConnection('retry', `Retrying in ${delay}ms (attempt ${this.retryCount + 1})`)
                setTimeout(() => this.connect(), delay)
            } else {
                this.logConnection('failed', 'Max retries exceeded')
            }
        }
    },

    async loadInitialData() {
        if (!this.connected) return

        try {
            this.syncStatus = 'syncing'
            this.logConnection('sync', 'Loading data...')

            // Simulate data loading with variable delay
            await this.delay(Math.random() * 1000 + 300)

            // Simulate occasional data errors
            if (Math.random() < 0.1) {
                throw new Error('Data fetch failed')
            }

            this.data = {
                lastSync: new Date(),
                items: this.generateSampleData(),
                version: Math.floor(Math.random() * 1000),
                checksum: this.generateChecksum()
            }

            this.lastSync = new Date()
            this.syncStatus = 'success'
            this.logConnection('sync_success', `Data loaded: ${this.data.items.length} items`)

        } catch (error) {
            this.error = error.message
            this.syncStatus = 'error'
            this.logConnection('sync_error', `Data load failed: ${error.message}`)
        }
    },

    async refreshData() {
        if (!this.connected) {
            this.connect()
            return
        }

        await this.loadInitialData()
    },

    setupPeriodicSync() {
        this.syncInterval = setInterval(() => {
            if (this.connected && this.syncStatus !== 'syncing') {
                this.loadInitialData()
            }
        }, 15000) // Sync every 15 seconds
    },

    setupConnectionMonitoring() {
        this.monitorInterval = setInterval(() => {
            if (this.connected) {
                // Simulate connection quality changes
                const qualityChange = Math.random() * 20 - 10
                this.connectionQuality = Math.round(Math.max(0, Math.min(100,
                    this.connectionQuality + qualityChange)))

                // Simulate latency changes
                this.latency = Math.round(Math.max(10,
                    this.latency + Math.random() * 40 - 20))

                // Simulate disconnection on poor quality
                if (this.connectionQuality < 20) {
                    this.disconnect()
                }
            }
        }, 5000) // Monitor every 5 seconds
    },

    disconnect() {
        if (this.connected) {
            this.connected = false
            this.connecting = false
            this.connectionQuality = 0
            this.logConnection('disconnected', 'Connection closed')

            // Auto-reconnect after a delay
            setTimeout(() => {
                if (!this.connected && !this.connecting) {
                    this.connect()
                }
            }, 3000)
        }
    },

    forceReconnect() {
        this.disconnect()
        setTimeout(() => this.connect(), 1000)
    },

    clearHistory() {
        this.connectionHistory = []
        this.logConnection('history_cleared', 'Connection history cleared')
    },

    logConnection(type, message) {
        const entry = {
            id: Date.now(),
            type,
            message,
            timestamp: new Date().toLocaleTimeString(),
            quality: this.connectionQuality,
            latency: this.latency
        }

        this.connectionHistory.unshift(entry)

        // Keep history manageable
        if (this.connectionHistory.length > 50) {
            this.connectionHistory = this.connectionHistory.slice(0, 50)
        }
    },

    generateSampleData() {
        const items = []
        const count = Math.floor(Math.random() * 10) + 5

        for (let i = 0; i < count; i++) {
            items.push({
                id: i + 1,
                name: `Item ${i + 1}`,
                value: Math.floor(Math.random() * 1000),
                type: ['A', 'B', 'C'][Math.floor(Math.random() * 3)]
            })
        }

        return items
    },

    generateChecksum() {
        return Math.random().toString(36).substring(7).toUpperCase()
    }
})
// Background Tasks Store - Task scheduling and lifecycle management
wildflower.store('background-tasks', {
    state: {
        tasks: [],
        runningTasks: 0,
        maxConcurrentTasks: 3,
        totalCompleted: 0,
        totalFailed: 0,
        scheduler: null,
        paused: false
    },
    
    computed: {
        pendingTasks() {
            return this.tasks.filter(t => t.status === 'pending')
        },

        runningTaskList() {
            return this.tasks.filter(t => t.status === 'running')
        },

        completedTasks() {
            return this.tasks.filter(t => t.status === 'completed')
        },

        failedTasks() {
            return this.tasks.filter(t => t.status === 'failed')
        },

        successRate() {
            const total = this.totalCompleted + this.totalFailed
            return total > 0 ? Math.round((this.totalCompleted / total) * 100) : 0
        }
    },

    init() {
        console.log('⏰ Background Tasks Store initializing...')
        this.startScheduler()
        this.addSampleTasks()
    },

    destroy() {
        console.log('🛑 Background Tasks Store destroying...')

        if (this.scheduler) {
            clearInterval(this.scheduler)
        }

        // Cancel all pending tasks
        this.tasks.forEach(task => {
            if (task.status === 'pending') {
                task.status = 'cancelled'
            }
        })
    },

    delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
    },

    // Methods at top level (Unified Entity Paradigm)
    addTask(task) {
        const newTask = {
            id: Date.now() + Math.random(),
            name: task.name,
            type: task.type || 'generic',
            priority: task.priority || 'normal',
            status: 'pending',
            progress: 0,
            duration: task.duration || 3000,
            createdAt: new Date(),
            startedAt: null,
            completedAt: null,
            error: null,
            result: null
        }

        this.tasks.push(newTask)
        this.sortTasksByPriority()
    },

    addSampleTasks() {
        const sampleTasks = [
            { name: 'Data Backup', type: 'backup', priority: 'high', duration: 5000 },
            { name: 'Cache Cleanup', type: 'maintenance', priority: 'normal', duration: 2000 },
            { name: 'Report Generation', type: 'report', priority: 'low', duration: 8000 },
            { name: 'Log Rotation', type: 'maintenance', priority: 'normal', duration: 1500 },
            { name: 'Security Scan', type: 'security', priority: 'high', duration: 7000 }
        ]

        sampleTasks.forEach(task => this.addTask(task))
    },

    sortTasksByPriority() {
        const priorityOrder = { 'high': 0, 'normal': 1, 'low': 2 }
        this.tasks.sort((a, b) => {
            if (a.status === 'pending' && b.status !== 'pending') return -1
            if (a.status !== 'pending' && b.status === 'pending') return 1
            return priorityOrder[a.priority] - priorityOrder[b.priority]
        })
    },

    async runTask(taskId) {
        const task = this.tasks.find(t => t.id === taskId)
        if (!task || task.status !== 'pending') return

        task.status = 'running'
        task.startedAt = new Date()
        task.progress = 0
        this.runningTasks++

        try {
            // Simulate task execution with progress updates
            const steps = 10
            for (let i = 0; i < steps; i++) {
                await this.delay(task.duration / steps)
                task.progress = Math.round(((i + 1) / steps) * 100)

                // Simulate occasional task failures
                if (Math.random() < 0.1) {
                    throw new Error('Task execution failed')
                }
            }

            task.status = 'completed'
            task.completedAt = new Date()
            task.result = `Task completed successfully in ${task.duration}ms`
            this.totalCompleted++

        } catch (error) {
            task.status = 'failed'
            task.error = error.message
            task.completedAt = new Date()
            this.totalFailed++
        }

        this.runningTasks--
    },

    startScheduler() {
        if (this.scheduler) return

        this.scheduler = setInterval(() => {
            if (this.paused) return

            // Find pending tasks that can be started
            const pendingTasks = this.tasks.filter(t => t.status === 'pending')
            const availableSlots = this.maxConcurrentTasks - this.runningTasks

            if (pendingTasks.length > 0 && availableSlots > 0) {
                const tasksToStart = pendingTasks.slice(0, availableSlots)
                tasksToStart.forEach(task => this.runTask(task.id))
            }
        }, 1000)
    },

    pauseScheduler() {
        this.paused = true
    },

    resumeScheduler() {
        this.paused = false
    },

    cancelTask(taskId) {
        const task = this.tasks.find(t => t.id === taskId)
        if (task && task.status === 'pending') {
            task.status = 'cancelled'
            task.completedAt = new Date()
        }
    },

    clearCompleted() {
        this.tasks = this.tasks.filter(t =>
            !['completed', 'failed', 'cancelled'].includes(t.status)
        )
    },

    addRandomTask() {
        const types = ['backup', 'maintenance', 'report', 'security', 'sync']
        const priorities = ['high', 'normal', 'low']

        this.addTask({
            name: `Random Task ${Math.floor(Math.random() * 1000)}`,
            type: types[Math.floor(Math.random() * types.length)],
            priority: priorities[Math.floor(Math.random() * priorities.length)],
            duration: Math.floor(Math.random() * 5000) + 1000
        })
    },

    resetStats() {
        this.totalCompleted = 0
        this.totalFailed = 0
    }
})
<div data-component="lifecycle-demo">
    <p class="text-muted">Demonstrates advanced store lifecycle management with background tasks and data services.</p>

    <div class="row">
        <div class="col-md-6">
            <!-- Data Service Status -->
            <div class="card mb-3">
                <div class="card-header d-flex justify-content-between align-items-center">
                    <h6 class="mb-0">Data Service</h6>
                    <span data-bind="$data-service.connectionStatus" 
                          data-bind-class="$data-service.connected ? 'badge bg-success' : $data-service.connecting ? 'badge bg-warning' : 'badge bg-danger'">
                        disconnected
                    </span>
                </div>
                <div class="card-body">
                    <!-- Connection Info -->
                    <div class="row mb-3">
                        <div class="col-4">
                            <small class="text-muted">Quality</small>
                            <div class="fw-bold">
                                <span data-bind="$data-service.connectionQuality">0</span>%
                                <span data-bind="$data-service.qualityLevel" 
                                      data-bind-class="$data-service.qualityLevel === 'excellent' ? 'text-success' : $data-service.qualityLevel === 'good' ? 'text-info' : $data-service.qualityLevel === 'fair' ? 'text-warning' : 'text-danger'">
                                    (poor)
                                </span>
                            </div>
                        </div>
                        <div class="col-4">
                            <small class="text-muted">Latency</small>
                            <div class="fw-bold"><span data-bind="$data-service.latency">0</span>ms</div>
                        </div>
                        <div class="col-4">
                            <small class="text-muted">Bandwidth</small>
                            <div class="fw-bold text-capitalize" data-bind="$data-service.bandwidth">high</div>
                        </div>
                    </div>
                    
                    <!-- Data Info -->
                    <div data-show="$data-service.data">
                        <div class="mb-3">
                            <h6>Data Status:</h6>
                            <div class="row">
                                <div class="col-6">
                                    <small class="text-muted">Items</small>
                                    <div class="fw-bold"><span data-bind="$data-service.data ? $data-service.data.items.length : 0">0</span></div>
                                </div>
                                <div class="col-6">
                                    <small class="text-muted">Age</small>
                                    <div class="fw-bold"><span data-bind="$data-service.dataAge">0</span>s</div>
                                </div>
                            </div>
                        </div>
                    </div>
                    
                    <!-- Error Display -->
                    <div data-show="$data-service.error" class="alert alert-danger py-2">
                        <small>Error: <span data-bind="$data-service.error"></span></small>
                    </div>
                    
                    <!-- Actions -->
                    <div class="d-grid gap-2">
                        <button data-action="refreshData" class="btn btn-primary btn-sm">
                            Refresh Data
                        </button>
                        <button data-action="forceReconnect" class="btn btn-warning btn-sm">
                            Force Reconnect
                        </button>
                        <button data-action="clearServiceHistory" class="btn btn-secondary btn-sm">
                            Clear History
                        </button>
                    </div>
                </div>
            </div>
            
            <!-- Connection History -->
            <div class="card">
                <div class="card-header">
                    <h6 class="mb-0">Connection History</h6>
                </div>
                <div class="card-body p-0">
                    <div style="max-height: 200px; overflow-y: auto;">
                        <div data-list="$data-service.recentHistory">
                            <template>
                                <div class="px-3 py-2 border-bottom small">
                                    <div class="d-flex justify-content-between">
                                        <span data-bind="type" class="fw-bold text-capitalize"></span>
                                        <span data-bind="timestamp" class="text-muted"></span>
                                    </div>
                                    <div data-bind="message" class="text-muted"></div>
                                </div>
                            </template>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        
        <div class="col-md-6">
            <!-- Background Tasks -->
            <div class="card mb-3">
                <div class="card-header d-flex justify-content-between align-items-center">
                    <h6 class="mb-0">Background Tasks</h6>
                    <span data-bind="$background-tasks.runningTasks" class="badge bg-info">0</span>
                </div>
                <div class="card-body">
                    <!-- Task Stats -->
                    <div class="row mb-3">
                        <div class="col-4">
                            <small class="text-muted">Pending</small>
                            <div class="fw-bold"><span data-bind="$background-tasks.pendingTasks.length">0</span></div>
                        </div>
                        <div class="col-4">
                            <small class="text-muted">Success Rate</small>
                            <div class="fw-bold"><span data-bind="$background-tasks.successRate">0</span>%</div>
                        </div>
                        <div class="col-4">
                            <small class="text-muted">Failed</small>
                            <div class="fw-bold text-danger"><span data-bind="$background-tasks.totalFailed">0</span></div>
                        </div>
                    </div>
                    
                    <!-- Scheduler Controls -->
                    <div class="mb-3">
                        <div class="d-flex justify-content-between align-items-center mb-2">
                            <span>Scheduler:</span>
                            <span data-bind="$background-tasks.paused ? 'PAUSED' : 'RUNNING'" 
                                  data-bind-class="$background-tasks.paused ? 'text-warning' : 'text-success'">
                                RUNNING
                            </span>
                        </div>
                            <button data-action="pauseScheduler" class="btn btn-warning btn-sm me-2">
                                Pause
                            </button>
                            <button data-action="resumeScheduler" class="btn btn-success btn-sm">
                                Resume
                            </button>
                    </div>
                    
                    <!-- Task Actions -->
                    <div class="d-grid gap-2">
                        <button data-action="addRandomTask" class="btn btn-primary btn-sm">
                            Add Random Task
                        </button>
                        <button data-action="clearCompleted" class="btn btn-secondary btn-sm">
                            Clear Completed
                        </button>
                        <button data-action="resetTaskStats" class="btn btn-danger btn-sm">
                            Reset Stats
                        </button>
                    </div>
                </div>
            </div>
            
            <!-- Running Tasks -->
            <div class="card">
                <div class="card-header">
                    <h6 class="mb-0">Active Tasks</h6>
                </div>
                <div class="card-body p-0">
                    <div style="max-height: 200px; overflow-y: auto;">
                        <div data-list="$background-tasks.runningTaskList">
                            <template>
                                <div class="px-3 py-2 border-bottom">
                                    <div class="d-flex justify-content-between align-items-center">
                                        <div>
                                            <strong data-bind="name"></strong>
                                            <br>
                                            <small class="text-muted">
                                                <span data-bind="type" class="text-capitalize"></span> • 
                                                <span data-bind="priority" class="text-capitalize"></span>
                                            </small>
                                        </div>
                                        <div class="text-end">
                                            <div data-bind="progress" class="fw-bold">0</div>
                                            <small class="text-muted">%</small>
                                        </div>
                                    </div>
                                    <div class="progress mt-2" style="height: 12px;">
                                        <div class="progress-bar bg-primary"
                                             data-bind-style="{ width: progress + '%' }">
                                        </div>
                                    </div>
                                </div>
                            </template>
                        </div>
                        <div data-show="$background-tasks.runningTaskList.length === 0" class="text-center p-3 text-muted">
                            No active tasks
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
wildflower.component('lifecycle-demo', {
    // Data Service actions
    refreshData() {
        const dataService = wildflower.getStore('data-service')
        dataService.refreshData()
    },
    
    forceReconnect() {
        const dataService = wildflower.getStore('data-service')
        dataService.forceReconnect()
    },
    
    clearServiceHistory() {
        const dataService = wildflower.getStore('data-service')
        dataService.clearHistory()
    },
    
    // Background Tasks actions
    addRandomTask() {
        const taskStore = wildflower.getStore('background-tasks')
        taskStore.addRandomTask()
    },
    
    pauseScheduler() {
        const taskStore = wildflower.getStore('background-tasks')
        taskStore.pauseScheduler()
    },
    
    resumeScheduler() {
        const taskStore = wildflower.getStore('background-tasks')
        taskStore.resumeScheduler()
    },
    
    clearCompleted() {
        const taskStore = wildflower.getStore('background-tasks')
        taskStore.clearCompleted()
    },
    
    resetTaskStats() {
        const taskStore = wildflower.getStore('background-tasks')
        taskStore.resetStats()
    }
})
Live Preview

Headless Simulations: tick() in a Store

Stores can implement tick(dt) to drive per-frame work that lives outside any component. The store owns the simulation state and advances it each animation frame; one or more components subscribe and render. Since the store outlives any view, you can switch routes, swap viewports, or render the same simulation in multiple components, and the simulation keeps running.

See the physarum-store demo for a complete example: a physics store with no DOM owns the agent grid and pheromone field, and a separate viewport component subscribes and renders. tick(dt) signature, dt clamping, and ordering rules are documented on the Lifecycle Hooks page.

Virtual Component Protection

Store components are automatically protected from garbage collection during DOM changes:

⚠️ Important: Store components have the isVirtual: true flag that protects them from being destroyed during modal operations, route changes, or other DOM manipulations. This ensures persistent global state.
// Internal store component structure
const storeInstance = {
    id: instanceId,
    name: storeName,
    state: reactiveState,
    stateManager: stateManager,
    definition: storeDefinition,
    context: storeContext,
    isVirtual: true  // This flag protects from DOM-based cleanup
}

// Framework garbage collection excludes virtual components
if (instance.isVirtual || instance.isPersistent) {
    // Don't garbage collect virtual/persistent components
    return
}

Store API Quick Reference

API Purpose
wildflower.store(name, config)Create a global store
wildflower.getStore(name)Get store instance
$storeName.propertyBind to store state in HTML
subscribe: ['storeName']Wait for store + enable this.stores
subscribe: { store: ['path'] }Wait + receive onStoreUpdate notifications
store.subscribe(path, callback)Programmatic subscription
watch: { 'store:name.path': fn }Watch store changes in component
Full API details: See Store API Reference for complete documentation of all store methods, subscription patterns, the $ shorthand, onStoreUpdate, and subscribe-wait timing.

Store Initialization and Readiness

Stores with async init() provide readiness APIs. Components using subscribe: automatically wait for stores before their init() runs. See Store API Reference for isReady(), waitForReady(), and timeout configuration.

Store-Backed List Patterns

When rendering lists from store data, you have three options. All three work correctly with full reactivity:

Option 1: $ Shorthand (Recommended)

The most direct approach - bind the list directly to store data:

<!-- Bind directly to store items -->
<div data-list="$cart.items">
    <template>
        <div class="item">
            <span data-bind="name"></span>
            <span data-bind="price"></span>
        </div>
    </template>
</div>

<!-- Bind to a computed store property -->
<div data-list="$inventory.availableItems">
    <template>...</template>
</div>

Option 2: Computed Property

Use a computed property that returns store data:

<div data-list="items">
    <template>
        <div class="item" data-bind="name"></div>
    </template>
</div>
wildflower.component('item-list', {
    state: {},
    computed: {
        items() {
            return wildflower.getStore('myStore').items
        }
    }
})

Option 3: Subscribe Pattern

Sync store data to component state using subscriptions:

wildflower.component('synced-list', {
    state: {
        items: []
    },
    init() {
        const store = wildflower.getStore('myStore')
        // Initial sync
        this.items = [...store.items]
        // Subscribe to changes - callback receives (newValue, oldValue)
        store.subscribe('items', (newItems) => {
            if (Array.isArray(newItems)) {
                this.items = [...newItems]
            }
        })
    }
})
Which pattern to choose?
  • $entity.path - Simplest for read-only bindings, no extra component code needed
  • computed - Clean when you need to derive values from store data
  • subscribe - Required for two-way sync when component has editable local state that mirrors store data
⚠️ When Subscribe is Required: If your component has a local editing state (like form inputs) that needs to stay in sync with store changes from other sources, you must use the subscribe pattern. The $entity.path syntax only provides one-way display binding.

Example: A settings panel inside a list item component that edits column names. When the store updates (from another component), the input field values need to update too.

Best Practices

✅ Do
  • Create focused, single-responsibility stores
  • Use computed properties for derived data
  • Implement store persistence for user settings
  • Use store-to-store communication for complex workflows
  • Leverage lifecycle hooks for initialization and cleanup
  • Trust the framework's virtual component protection
❌ Don't
  • Create monolithic stores mixing unrelated data
  • Mutate store state directly from components
  • Forget to handle async operations properly
  • Create circular dependencies between stores
  • Store temporary UI state in global stores
  • Manually manage store cleanup (framework handles it)