Skip to content

Sponsor

Sponsor

Understanding createState() - A Beginner's Guide

Quick Start (30 seconds)

Want to create reactive state that automatically updates your DOM? Here's how:

js
// Create reactive state with auto-updating DOM bindings
const app = createState(
  { count: 0, message: 'Hello' },  // Your state
  {
    '#counter': 'count',            // Bind #counter to count
    '#display': 'message'           // Bind #display to message
  }
);

// Just change values - DOM updates automatically!
app.count = 5;        // #counter shows "5"
app.message = 'Hi';   // #display shows "Hi"

That's it! The createState() function creates reactive state and automatically wires up DOM bindings. Change your data, and the UI updates itself!

What is createState()?

createState() is a convenience function that combines two powerful features:

  1. Reactive state (like state())
  2. Automatic DOM bindings (connects your data to HTML elements)

In simple terms: It creates reactive data and automatically hooks it up to your page, so when the data changes, the HTML updates by itself.

Think of it as state() with built-in UI synchronization - you get reactive data management plus automatic DOM updates in one call.

Syntax

js
// Using the shortcut
createState(initialState, bindingDefs)

// Using the full namespace
ReactiveUtils.createState(initialState, bindingDefs)

Both styles are valid! Choose whichever you prefer:

  • Shortcut style (createState()) - Clean and concise
  • Namespace style (ReactiveUtils.createState()) - Explicit and clear

Parameters:

  • initialState - An object with your initial data (required)
  • bindingDefs - An object mapping selectors to state properties (optional)

Returns:

  • A reactive state object with automatic DOM bindings active

Why Does This Exist?

When Declarative Bindings Shine

The Reactive library offers flexible ways to connect state to your UI. Sometimes you want full control with manual effects:

javascript
// Create reactive state
const app = state({ count: 0 });

// Explicitly control DOM updates
effect(() => {
  document.getElementById('counter').textContent = app.count;
});

app.count = 5; // DOM updates via your effect

This approach is great when you need: ✅ Fine-grained control over updates ✅ Complex update logic ✅ Conditional rendering ✅ Custom transformation of values

When You Want Declarative Simplicity

In scenarios where you have straightforward state-to-DOM mappings, createState() provides a more direct approach:

javascript
// State and bindings declared together
const app = createState(
  { count: 0 },
  {
    '#counter': 'count'  // Declarative binding
  }
);

app.count = 5; // DOM updates automatically

This method is especially useful when:

Declarative Binding Flow:
┌──────────────────┐
│ createState()    │
│                  │
│ State + Bindings │
│ in one place     │
└────────┬─────────┘


   Everything setup
   automatically


  ✅ Clear & concise

Where createState() shines: ✅ Simple property-to-element mappings ✅ Reducing boilerplate for common patterns ✅ Keeping state and UI connections co-located ✅ Projects where declarative style is preferred ✅ Quick prototypes and straightforward UIs

The Choice is Yours:

  • Use state() + effect() when you need flexibility and control
  • Use createState() when you want declarative simplicity
  • Both approaches are valid and can even be mixed in the same project

Benefits of the declarative approach: ✅ State and bindings defined together ✅ Less code for straightforward cases ✅ Clear data → UI mapping at a glance ✅ Automatic synchronization handled for you

Mental Model

Think of createState() like a smart home hub with pre-wired rooms:

Regular state() (Manual Wiring):
┌──────────────────────┐
│   Smart Home Hub     │
│   (State)            │
└──────────────────────┘

         │ You must manually wire each device:

         ├→ Wire light switch
         ├→ Wire thermostat
         ├→ Wire alarm
         └→ Wire camera

createState() (Pre-Wired):
┌──────────────────────────────────┐
│   Smart Home Hub                 │
│   (State)                        │
│                                  │
│   Pre-wired connections:         │
│   ├→ Kitchen light → Switch 1   │
│   ├→ Temperature → Thermostat   │
│   ├→ Security → Alarm           │
│   └→ Front door → Camera        │
└──────────────────────────────────┘


   Everything connected
   automatically at setup!

Key Insight: Just like a smart home hub that comes pre-wired to your devices, createState() sets up all your data-to-DOM connections in one step. You don't have to manually wire each connection later.

How Does It Work?

The Magic: Combining Two Features

When you call createState(), here's what happens behind the scenes:

javascript
// What you write:
const app = createState(
  { count: 0 },
  { '#counter': 'count' }
);

// What actually happens (simplified):
// 1. Create reactive state
const app = state({ count: 0 });

// 2. Create bindings automatically
app.bind({
  '#counter': 'count'
});

In other words: createState() is a convenience wrapper that:

  1. Creates reactive state using state()
  2. Automatically sets up DOM bindings using ReactiveUtils.set() with bindings
  3. Returns the state object with everything wired up

Under the Hood

createState({ count: 0 }, { '#counter': 'count' })


┌───────────────────────┐
│  Step 1: Create State │
│  state({ count: 0 })  │
└──────────┬────────────┘


┌───────────────────────┐
│  Step 2: Parse        │
│  Binding Definitions  │
└──────────┬────────────┘


┌───────────────────────┐
│  Step 3: Query DOM    │
│  #counter element     │
└──────────┬────────────┘


┌───────────────────────┐
│  Step 4: Create       │
│  Effect for Binding   │
└──────────┬────────────┘


┌───────────────────────┐
│  Step 5: Return       │
│  Connected State      │
└───────────────────────┘

What happens:

1️⃣ When you read app.count, the Proxy notices and tracks dependencies 2️⃣ When you write app.count = 5, the Proxy notices and triggers effects 3️⃣ The binding effect runs automatically and updates #counter element 4️⃣ Your UI stays synchronized with your data - no manual work!

Basic Usage

Creating State with Bindings

The simplest way to use createState() is with ID selectors:

js
// HTML:
// <div id="username"></div>
// <div id="email"></div>

// JavaScript:
const user = createState(
  {
    name: 'John',
    email: 'john@example.com'
  },
  {
    '#username': 'name',
    '#email': 'email'
  }
);

// Elements automatically show:
// #username: "John"
// #email: "john@example.com"

Updating State (DOM Updates Automatically)

js
// Change the values
user.name = 'Jane';
user.email = 'jane@example.com';

// DOM automatically updates to show:
// #username: "Jane"
// #email: "jane@example.com"

No manual DOM manipulation needed! Just change your data, and the UI follows.

Understanding Bindings

What is a Binding?

A binding connects a piece of state to a DOM element. When the state changes, the element updates automatically.

Anatomy of a Binding:

js
{
  '#counter': 'count'
  //  │         │
  //  │         └─ State property to watch
  //  └─────────── CSS selector for element
}
  • Left side (key): CSS selector for the target element
  • Right side (value): Property name in your state

Simple Property Binding

js
const app = createState(
  { message: 'Hello' },
  { '#display': 'message' }
);

// Behind the scenes, this creates:
effect(() => {
  document.getElementById('display').textContent = app.message;
});

Function Binding (Computed Values)

You can also bind to computed values using a function:

js
const app = createState(
  { firstName: 'John', lastName: 'Doe' },
  {
    '#fullName': function() {
      return this.firstName + ' ' + this.lastName;
    }
  }
);

// #fullName shows: "John Doe"

app.firstName = 'Jane';
// #fullName automatically updates to: "Jane Doe"

Object Binding (Multiple Properties)

Bind multiple properties on the same element:

js
const app = createState(
  { isActive: true, count: 5 },
  {
    '#display': {
      textContent: 'count',
      className: function() {
        return this.isActive ? 'active' : 'inactive';
      }
    }
  }
);

// #display element gets:
// - textContent = "5"
// - className = "active"

Binding Patterns

Pattern 1: Simple Text Content

js
// HTML: <div id="message"></div>

const app = createState(
  { text: 'Hello World' },
  { '#message': 'text' }
);

// Result: #message shows "Hello World"

Pattern 2: Computed Text

js
// HTML: <div id="greeting"></div>

const app = createState(
  { name: 'John', time: 'morning' },
  {
    '#greeting': function() {
      return `Good ${this.time}, ${this.name}!`;
    }
  }
);

// Result: #greeting shows "Good morning, John!"

Pattern 3: Multiple Selectors

js
// HTML:
// <div id="display1"></div>
// <div id="display2"></div>

const app = createState(
  { count: 0 },
  {
    '#display1': 'count',
    '#display2': 'count'
  }
);

// Both elements show the same value

Pattern 4: Class Selectors

js
// HTML:
// <div class="counter"></div>
// <div class="counter"></div>

const app = createState(
  { count: 0 },
  {
    '.counter': 'count'  // Binds to ALL .counter elements
  }
);

// All .counter elements show: "0"

Pattern 5: Complex Selectors

js
// HTML: <div data-display="counter"></div>

const app = createState(
  { count: 0 },
  {
    '[data-display="counter"]': 'count'
  }
);

Advanced Binding Examples

Example 1: Counter with Multiple Updates

js
// HTML:
// <div id="count"></div>
// <div id="doubled"></div>
// <div id="status"></div>

const counter = createState(
  { value: 0 },
  {
    '#count': 'value',
    '#doubled': function() {
      return this.value * 2;
    },
    '#status': function() {
      return this.value > 5 ? 'High' : 'Low';
    }
  }
);

// Change value once:
counter.value = 8;

// All three elements update automatically:
// #count: "8"
// #doubled: "16"
// #status: "High"

Example 2: Conditional Classes

js
// HTML: <button id="toggleBtn">Click me</button>

const app = createState(
  { isActive: false },
  {
    '#toggleBtn': {
      className: function() {
        return this.isActive ? 'btn-active' : 'btn-inactive';
      },
      textContent: function() {
        return this.isActive ? 'Active' : 'Inactive';
      }
    }
  }
);

// Toggle button handler
document.getElementById('toggleBtn').onclick = () => {
  app.isActive = !app.isActive;
};

Example 3: Style Binding

js
// HTML: <div id="box"></div>

const app = createState(
  { width: 100, color: 'blue' },
  {
    '#box': {
      style: function() {
        return {
          width: this.width + 'px',
          height: this.width + 'px',
          backgroundColor: this.color
        };
      }
    }
  }
);

app.width = 200;  // Box resizes automatically
app.color = 'red'; // Box changes color automatically

Example 4: Array Display

js
// HTML: <div id="list"></div>

const app = createState(
  { items: ['Apple', 'Banana', 'Cherry'] },
  {
    '#list': function() {
      return this.items.join(', ');
    }
  }
);

// #list shows: "Apple, Banana, Cherry"

app.items.push('Date');
// #list shows: "Apple, Banana, Cherry, Date"

createState() vs state()

When to Use createState()

Use createState() when you need automatic DOM bindings:

✅ Simple UI updates ✅ Forms that sync to display ✅ Dashboards with live data ✅ Any app where state → DOM connection is straightforward

js
// Perfect for simple bindings
const app = createState(
  { count: 0 },
  { '#counter': 'count' }
);

When to Use state()

Use state() when you need manual control over updates:

✅ Complex rendering logic ✅ Conditional updates ✅ Custom DOM manipulation ✅ Non-DOM side effects

js
// Better for complex logic
const app = state({ count: 0 });

effect(() => {
  if (app.count > 10) {
    // Complex logic
    doSomethingSpecial();
  } else {
    // Different logic
    doSomethingElse();
  }
});

Quick Comparison

javascript
// ❌ Using state() with manual binding (verbose)
const app = state({ count: 0 });

effect(() => {
  document.getElementById('counter').textContent = app.count;
});

effect(() => {
  document.getElementById('doubled').textContent = app.count * 2;
});

// ✅ Using createState() with auto-binding (clean)
const app = createState(
  { count: 0 },
  {
    '#counter': 'count',
    '#doubled': function() { return this.count * 2; }
  }
);

Simple Rule:

  • Need DOM bindings? Use createState()
  • Need custom logic? Use state() + effect()
  • Need both? Use createState() for bindings, effect() for logic

Common Patterns

Pattern: Form Input Sync

js
// HTML:
// <input id="nameInput" type="text">
// <div id="nameDisplay"></div>

const form = createState(
  { name: '' },
  { '#nameDisplay': 'name' }
);

// Connect input to state
document.getElementById('nameInput').addEventListener('input', (e) => {
  form.name = e.target.value;
});

// #nameDisplay automatically shows what's typed

Pattern: Live Counter

js
// HTML:
// <button id="increment">+</button>
// <div id="count"></div>
// <button id="decrement">-</button>

const counter = createState(
  { value: 0 },
  { '#count': 'value' }
);

document.getElementById('increment').onclick = () => {
  counter.value++;
};

document.getElementById('decrement').onclick = () => {
  counter.value--;
};

Pattern: Status Dashboard

js
// HTML:
// <div id="temperature"></div>
// <div id="humidity"></div>
// <div id="status"></div>

const dashboard = createState(
  { temp: 20, humidity: 45 },
  {
    '#temperature': function() {
      return this.temp + '°C';
    },
    '#humidity': function() {
      return this.humidity + '%';
    },
    '#status': function() {
      return this.temp > 25 && this.humidity > 60
        ? 'Hot & Humid'
        : 'Comfortable';
    }
  }
);

// Update data from API
setInterval(() => {
  dashboard.temp = Math.round(Math.random() * 30);
  dashboard.humidity = Math.round(Math.random() * 100);
}, 5000);

Pattern: Shopping Cart Total

js
// HTML:
// <div id="items"></div>
// <div id="total"></div>

const cart = createState(
  { items: [], price: 10 },
  {
    '#items': function() {
      return this.items.length + ' items';
    },
    '#total': function() {
      return '$' + (this.items.length * this.price).toFixed(2);
    }
  }
);

// Add to cart
function addItem(item) {
  cart.items.push(item);
}

Common Pitfalls

Pitfall #1: Element Not Found

Wrong:

js
// Element doesn't exist yet
const app = createState(
  { count: 0 },
  { '#counter': 'count' }
);

// Later in HTML:
// <div id="counter"></div>

The binding fails silently because the element doesn't exist when createState() runs.

Correct:

js
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', () => {
  const app = createState(
    { count: 0 },
    { '#counter': 'count' }
  );
});

// Or place script at end of body

Pitfall #2: Wrong Selector

Wrong:

js
// HTML: <div id="counter"></div>

const app = createState(
  { count: 0 },
  { 'counter': 'count' }  // Missing #
);

Without #, it looks for a <counter> tag, not an ID.

Correct:

js
const app = createState(
  { count: 0 },
  { '#counter': 'count' }  // Use # for IDs
);

Pitfall #3: Binding to Non-Existent Property

Wrong:

js
const app = createState(
  { count: 0 },
  { '#display': 'number' }  // Property 'number' doesn't exist
);

This will display undefined because app.number doesn't exist.

Correct:

js
const app = createState(
  { count: 0 },
  { '#display': 'count' }  // Use existing property name
);

Pitfall #4: Function Context Issues

Wrong:

js
const app = createState(
  { count: 0 },
  {
    '#display': () => {
      return this.count * 2;  // Arrow function: 'this' is wrong!
    }
  }
);

Arrow functions don't bind this correctly.

Correct:

js
const app = createState(
  { count: 0 },
  {
    '#display': function() {
      return this.count * 2;  // Regular function: 'this' works!
    }
  }
);

Pitfall #5: Forgetting Bindings Are Optional

js
// You can use createState WITHOUT bindings
const app = createState({ count: 0 });

// It works just like state()
app.count = 5;

// Manually bind later if needed
effect(() => {
  document.getElementById('counter').textContent = app.count;
});

Bindings are optional! If you don't provide them, createState() works exactly like state().

Summary

What is createState()?

createState() creates reactive state with automatic DOM bindings. It combines state() with built-in UI synchronization.

Why use createState() instead of state()?

  • Less boilerplate for DOM updates
  • Declarative binding syntax
  • Clear data → UI mapping
  • Automatic synchronization

Key Points to Remember:

1️⃣ Bindings are declarations - define them upfront with the state 2️⃣ Selectors can be IDs, classes, or complex - #id, .class, [attr] 3️⃣ Values can be properties or functions - static or computed 4️⃣ DOM must exist before calling createState() 5️⃣ Use regular functions for computed bindings, not arrow functions

Mental Model: Think of createState() as a smart home hub with pre-wired connections - everything is connected automatically at setup, so changes flow from data to UI instantly.

Quick Reference:

js
// Simple binding
const app = createState(
  { count: 0 },
  { '#counter': 'count' }
);

// Computed binding
const app = createState(
  { count: 0 },
  {
    '#display': function() {
      return this.count * 2;
    }
  }
);

// Multiple properties
const app = createState(
  { count: 0, isActive: true },
  {
    '#display': {
      textContent: 'count',
      className: function() {
        return this.isActive ? 'active' : '';
      }
    }
  }
);

// Multiple elements
const app = createState(
  { count: 0 },
  {
    '#display1': 'count',
    '#display2': 'count',
    '.counter': 'count'
  }
);

Remember: createState() is perfect when you need reactive state with automatic DOM updates. It saves you from writing repetitive effect() code and makes your data-to-UI connections clear and maintainable!