Skip to content

Sponsor

Sponsor

Understanding state() - A Beginner's Guide

Quick Start (30 seconds)

Want to make your JavaScript objects reactive? Here's how:

js
// Create reactive state
const user = state({
  name: 'John',
  age: 25
});

// Automatically update UI when data changes
effect(() => {
  document.getElementById('display').textContent = user.name;
});

// Just change the value - UI updates automatically!
user.name = 'Jane'; // Display updates to "Jane"

That's it! The state() function transforms regular objects into reactive objects that automatically notify your UI when values change.

What is state()?

state() is the most fundamental function in the Reactive library. It takes a regular JavaScript object and transforms it into a reactive object - one that can automatically detect when its properties change and trigger updates throughout your application.

A reactive state:

  • Detects when properties are read
  • Detects when properties are writes
  • Automatically notifies effects, computed values, and watchers

Think of it as upgrading a plain object to a smart object - it becomes self-aware and can notify other parts of your code when something changes.

Syntax

js
// Using the shortcut
state({ property: value, ... })

// Using the full namespace
ReactiveUtils.state({ property: value, ... })

Both styles are valid! Choose whichever you prefer:

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

Parameters:

  • An object with initial properties and values

Returns:

  • A reactive proxy object with the same properties, plus special methods (starting with $)

Why Does This Exist?

The Problem with Regular Objects

Let's say you have a regular JavaScript object:

javascript
// Regular object - no special powers
const user = {
  name: 'John',
  age: 25
};

// You can read values
console.log(user.name); // "John"

// You can update values
user.name = 'Jane'; // Changed, but nobody knows!
console.log(user.name); // "Jane"

At first glance, this looks perfectly fine. JavaScript lets you read and write values easily. But there's a hidden limitation.

What's the Real Issue?

Regular Object Change Flow:
┌─────────────┐
│ user.name   │
│   = 'Jane'  │
└──────┬──────┘


   [SILENCE]


 Nothing happens
 No notifications
 No UI updates
 No side effects

Problems:

  • When you change user.name, nothing else in your code knows about it - the change happens silently
  • JavaScript does not notify other parts of your code that something changed
  • You can't automatically update the screen
  • You can't automatically run code when values change
  • You have to manually check for changes everywhere
  • The object changes, but nothing reacts to it

Why This Becomes a Problem:

As your app grows, this leads to several issues:

❌ Changes are invisible to the rest of your application ❌ The UI doesn't update unless you manually tell it to ❌ You can't easily run side effects when data changes ❌ You end up writing extra code just to "check" for changes ❌ Data and UI easily get out of sync

In other words, regular objects have no awareness of change. They store data — but they don't communicate.

The Solution with state()

When you wrap an object with state(), it becomes reactive:

javascript
// Reactive object - now it has superpowers! ✨
const user = state({
  name: 'John',
  age: 25
});

// You can now attach logic that automatically runs whenever the state changes
effect(() => {
  console.log('Name is: ' + user.name);
});
// Immediately logs: "Name is: John"

// Now change the value
user.name = 'Jane';
// Automatically logs: "Name is: Jane"

What Just Happened?

Reactive State Change Flow:
┌─────────────┐
│ user.name   │
│   = 'Jane'  │
└──────┬──────┘


 [PROXY DETECTS]


 Notifies all
 watching effects


✅ UI updates
✅ Side effects run
✅ Computed values refresh

With state():

  • Changes are detected automatically
  • Any code that depends on the changed value re-runs by itself
  • You don't need to manually track or trigger updates
  • The UI stays in sync with your data

Benefits:

  • ✅ Changes are automatically detected
  • ✅ Code can automatically respond to changes
  • ✅ You can build reactive user interfaces
  • ✅ Less manual work, fewer bugs
  • ✅ Data and UI stay synchronized

Mental Model

Think of state() like a smart home system:

Regular Object (Dumb House):
┌──────────────────────┐
│  Temperature: 20°C   │  ← You can read it
│  Lights: On          │  ← You can change it
│  Door: Closed        │
└──────────────────────┘
     No sensors
     No automation
     No reactions

Reactive State (Smart House):
┌──────────────────────┐
│  Temperature: 20°C   │ ←─┐
│  Lights: On          │ ←─┼─ Sensors watching
│  Door: Closed        │ ←─┘
└──────────────────────┘


   ┌─────────────┐
   │  Controller │
   └──────┬──────┘


When temperature changes:
  ✓ Thermostat adjusts
  ✓ Notification sent
  ✓ Logs recorded

When door opens:
  ✓ Lights turn on
  ✓ Alarm checks status
  ✓ Camera starts recording

Key Insight: Just like a smart home automatically reacts to changes (door opens → lights turn on), reactive state automatically triggers updates (data changes → UI updates).

How Does It Work?

The Magic: JavaScript Proxies

When you call state(), it doesn't just return your object - it wraps it in a Proxy, a smart layer that can watch what you do

Think of a Proxy as a smart wrapper that sits between you and your data:

You  →  [Proxy Wrapper]  →  Your Data

       Watches reads
       Watches writes

You still interact with your data normally — but now it’s being observed.

What Is a Proxy?

A Proxy is a JavaScript feature that lets code intercept actions on an object.

That means it can:

  • Notice when you read a value
  • Notice when you change a value
  • Run extra logic automatically

A Proxy does not change your object's shape or syntax. It only listens and reacts.

What happens:

  1. When you read a property (user.name), the Proxy notices and tracks it
  2. When you write a property (user.name = 'Jane'), the Proxy notices and triggers updates
  3. Any code that depends on that property automatically re-runs

This is completely transparent - you use the reactive object exactly like a normal object, but it has special powers behind the scenes!

Under the Hood:

state({ name: 'John' })


┌───────────────────┐
│   Proxy Layer     │
├───────────────────┤
│ GET handler  ───► Track dependency
│ SET handler  ───► Trigger effects
│ DELETE handler ─► Trigger effects
└───────────────────┘


┌───────────────────┐
│  Original Object  │
│  { name: 'John' } │
└───────────────────┘

Basic Usage

Creating Reactive State

The simplest way to use state() is to wrap a plain JavaScript object:

js
// Using the shortcut style
const myState = state({
  message: 'Hello',
  count: 0,
  isActive: true
});

// Or using the namespace style
const myState = state({
  message: 'Hello',
  count: 0,
  isActive: true
});

That's it! Now myState is reactive - it can detect and respond to changes.

Accessing Properties

What Does "Accessing Properties" Mean?

When you have an object with data inside it, accessing a property simply means reading or getting the value stored in that object.

Think of an object like a box with labeled compartments. Each compartment (property) holds a value.

Basic Example: Regular Objects

First, let's look at a normal JavaScript object:

javascript
// Create a regular object
const person = {
  name: 'Alice',
  age: 25,
  city: 'New York'
};

// Access properties using the dot (.)
console.log(person.name);  // Output: "Alice"
console.log(person.age);   // Output: 25
console.log(person.city);  // Output: "New York"

What's happening?

  • person.name means: "Give me the value stored in the 'name' property"
  • It's like saying: "Open the box labeled 'person' and look in the compartment labeled 'name'"

Reactive State Objects Work The Same Way!

When you create a reactive state object, you access its properties exactly the same way:

javascript
// Create a reactive state object
const myState = state({
  message: 'Hello',
  count: 0,
  isActive: true
});

// Access properties - works just like regular objects!
console.log(myState.message);  // Output: "Hello"
console.log(myState.count);    // Output: 0
console.log(myState.isActive); // Output: true

The magic: Even though myState is "reactive" (it tracks changes), you still read values from it the normal way!

Using Property Values in Your Code

Once you access a property, you can use that value anywhere:

javascript
const myState = state({
  message: 'Hello',
  count: 5,
  price: 19.99
});

// ✅ Use in string concatenation
const greeting = myState.message + ', World!';
console.log(greeting);  // Output: "Hello, World!"

// ✅ Use in math operations
const doubled = myState.count * 2;
console.log(doubled);  // Output: 10

// ✅ Use in conditions
if (myState.count > 0) {
  console.log('Count is positive!');
}

// ✅ Use in calculations
const total = myState.price * myState.count;
console.log(total);  // Output: 99.95

Key Takeaway

Reactive state objects are friendly!

Even though they have special powers (like automatically updating your UI when values change), you interact with them using the same simple syntax you already know:

javascript
object.propertyName  // That's it!

No special functions needed to read values. Just use the dot notation like always! 🎉

Visual Comparison

javascript
// Regular Object
const regularObj = { name: 'Bob' };
console.log(regularObj.name);  // ✅ Works

// Reactive Object  
const reactiveObj = state({ name: 'Bob' });
console.log(reactiveObj.name); // ✅ Works the same way!

Both look and feel identical when you're reading values. The difference only shows up when you change values (that's when the reactive magic happens).

Updating Properties

What Does "Updating Properties" Mean?

Updating (or changing) a property means giving it a new value. Think of it like replacing what's inside a labeled box.

Regular Objects: How Updating Works

With normal JavaScript objects, you can change values using the equals sign =:

javascript
// Create a regular object
const person = {
  name: 'Alice',
  age: 25
};

// Update properties
person.name = 'Bob';      // Change name
person.age = 30;          // Change age

console.log(person.name); // Output: "Bob"
console.log(person.age);  // Output: 30

That's it! You just assign a new value with =.

Reactive State: Updating Works The Same Way!

Great news: reactive state objects update exactly like regular objects:

javascript
// Create reactive state
const myState = state({
  message: 'Hello',
  count: 0,
  isActive: true
});

// Update properties - same syntax!
myState.message = 'Hi there!';
myState.count = 5;
myState.isActive = false;

console.log(myState.message); // Output: "Hi there!"
console.log(myState.count);   // Output: 5
console.log(myState.isActive); // Output: false

No special functions needed! Just use = like always.

All The Ways You Can Update

You can update reactive properties using any method that works with regular objects:

Basic Assignment

javascript
myState.count = 10;          // Set to a specific value
myState.message = 'Goodbye'; // Replace text

Increment & Decrement

javascript
myState.count++;  // Increase by 1 (same as: count = count + 1)
myState.count--;  // Decrease by 1 (same as: count = count - 1)

Arithmetic Operations

javascript
myState.count = myState.count + 5;  // Add 5
myState.count = myState.count * 2;  // Double it
myState.count = myState.count / 2;  // Divide by 2

// Shorthand versions
myState.count += 5;  // Same as: count = count + 5
myState.count *= 2;  // Same as: count = count * 2

String Operations

javascript
myState.message = myState.message + ' World';  // Concatenate
myState.message += '!';                         // Append
myState.message = myState.message.toUpperCase(); // Transform

🌟 The Big Difference: Automatic Reactions!

Here's where reactive state gets magical:

Regular Objects

javascript
const regular = { count: 0 };

// Update the value
regular.count = 5;

// ❌ Nothing happens automatically
// You have to manually update the UI, run calculations, etc.

Reactive Objects

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

// Register something that should happen when count changes
effect(() => {
  console.log(`Count is now: ${reactive.count}`);
});
// Output immediately: "Count is now: 0"

// Update the value
reactive.count = 5;
// Output automatically: "Count is now: 5" ✨

// Update again
reactive.count = 10;
// Output automatically: "Count is now: 10" ✨

What happened?

  • When you changed reactive.count, the effect automatically re-ran!
  • You didn't have to manually call anything
  • The reactive system detected the change and responded

Real-World Example: Counter App

javascript
// Create reactive state for a counter
const counter = state({ value: 0 });

// Set up automatic UI update
effect(() => {
  document.getElementById('display').textContent = counter.value;
});

// When button is clicked:
counter.value++;  // UI automatically updates! ✨

Without reactive state, you'd have to do:

javascript
let value = 0;

function increment() {
  value++;
  // ❌ Manually update UI every time
  document.getElementById('display').textContent = value;
}

With reactive state, updates are automatic! 🎉

Key Takeaways

Updating syntax is the same: Use = just like regular objects

All normal operations work: ++, +=, *=, etc.

The magic: Changes automatically trigger effects

You write less code: No manual UI updates needed

Quick Comparison

javascript
// ❌ Regular Object (manual work)
const regular = { count: 0 };
regular.count = 5;
updateUI();  // You must remember to do this!

// ✅ Reactive Object (automatic)
const reactive = state({ count: 0 });
reactive.count = 5;  // UI updates automatically!

The syntax is identical, but reactive objects do the heavy lifting for you! 🚀

Using with Effects

State becomes truly powerful when combined with effects:

javascript
const counter = state({
  count: 0
});

Here, counter is no longer a plain object. It is a reactive proxy that can:

  • Detect when properties are read
  • Detect when properties are written
  • Notify any dependent logic when a value changes

At this point, nothing reacts yet — the state is just ready.

An effect is a function that automatically re-runs whenever the reactive data it uses changes. You don't tell it when to run — it figures that out by itself.

javascript
// Effect runs whenever counter.count changes
effect(() => {
  console.log('Count is now: ' + counter.count);
  document.getElementById('display').textContent = counter.count;
});

This effect does two important things:

  • Reads counter.count
  • Uses that value to:
    • Log to the console
    • Update the DOM

Because counter.count is read inside the effect, the reactive system automatically:

  • Registers the effect as a dependency of counter.count
  • Remembers: "This effect depends on counter.count"

The effect also runs immediately once to establish the initial state.

javascript
// Changes automatically trigger the effect
counter.count = 5;  // Effect runs, updates DOM
counter.count = 10; // Effect runs again

When these lines execute:

  • The proxy detects a write to counter.count
  • The reactive system looks up: "Who depends on counter.count?"
  • All matching effects are automatically re-run

Dependency Tracking Illustration

Think of the reactive system as maintaining a dependency map:

counter.count → [Effect #1, Effect #2, Effect #3]

    └─ When counter.count changes, notify all subscribers

Here's a more concrete example with multiple effects:

javascript
const counter = state({ count: 0 });

// Effect #1: Updates console
effect(() => {
  console.log('Count: ' + counter.count);
});

// Effect #2: Updates DOM element
effect(() => {
  document.getElementById('display').textContent = counter.count;
});

// Effect #3: Checks if count is even
effect(() => {
  const isEven = counter.count % 2 === 0;
  console.log('Is even: ' + isEven);
});

Dependency map after setup:

counter.count
    ├─→ Effect #1 (console log)
    ├─→ Effect #2 (DOM update)
    └─→ Effect #3 (even check)

What happens when you write counter.count = 5:

  1. Proxy intercepts: "Someone is writing to counter.count"
  2. System looks up: "I have 3 effects watching counter.count"
  3. Notification cascade: All three effects re-run automatically
  4. Result: Console shows new count, DOM updates, even check runs

This is why you never need to manually call effects or wire up event listeners — the reactive system maintains these relationships and handles notifications for you.

Result:

  • Console updates
  • DOM updates
  • No manual calls
  • No event wiring

Deep Reactivity

State objects support deep reactivity - nested objects are automatically made reactive: Reactive state in this system is deep by default. This means that nested objects automatically participate in reactivity, without any extra configuration or manual wrapping.

You don't need to call state() for every level — the system handles it for you.

Although only the top-level object is passed to state(), all nested objects (user, address) become reactive as well. There is no difference in how you use them — you access properties normally.

javascript
const app = state({
  user: {
    name: 'John',
    address: {
      city: 'New York',
      country: 'USA'
    }
  }
});

What matters here is what gets read inside the effect:

  1. app.user
  2. app.user.address
  3. app.user.address.city

Each read is automatically tracked by the reactive system. The effect is now subscribed specifically to address.city.

javascript
effect(() => {
  console.log('City: ' + app.user.address.city);
});

Deep changes are tracked! Even though this change happens several levels deep:

  1. The proxy detects the write
  2. The system finds all effects that depend on address.city
  3. Those effects re-run automatically
javascript
app.user.address.city = 'Los Angeles'; // Effect re-runs!

How it works:

  • When you access a nested object, it's automatically converted to a reactive proxy
  • All levels of nesting are reactive
  • You can track changes at any depth

Why This Matters

Without deep reactivity, you would need to manually wrap every nested level in state():

javascript
const app = state({
  user: state({
    address: state({
      city: 'New York'
    })
  })
});

This becomes tedious and error-prone, especially with deeply nested data structures. You'd need to remember to wrap every object at every level, or reactivity would break at that point.

Deep reactivity removes that complexity entirely. When you create a reactive state object, the system automatically makes all nested objects reactive too. You write code the way you naturally think about data structures.

The Difference in Practice

Without deep reactivity (manual wrapping):

javascript
// You have to wrap each level
const app = state({
  user: state({
    profile: state({
      settings: state({
        theme: 'dark'
      })
    })
  })
});

With deep reactivity (automatic):

javascript
// Just wrap the top level, everything else is handled
const app = state({
  user: {
    profile: {
      settings: {
        theme: 'dark'
      }
    }
  }
});

// Changes at any depth still work
app.user.profile.settings.theme = 'light'; // ✅ Triggers effects

Benefits

  • Clean, natural data structures — Write objects the way you normally would
  • No repetitive state() calls — One call at the top level is enough
  • Works with real-world nested data — API responses, configurations, and data models often have deep nesting
  • Fine-grained updates — Only effects that depend on the specific changed property re-run, not everything

Real-World Example

When you fetch data from an API, it often comes deeply nested:

javascript
const app = state({
  currentUser: {
    id: 123,
    profile: {
      name: 'Jane Doe',
      avatar: 'avatar.jpg'
    },
    preferences: {
      notifications: {
        email: true,
        push: false
      }
    }
  }
});

// This works immediately, no extra setup
effect(() => {
  if (app.currentUser.preferences.notifications.email) {
    console.log('Email notifications enabled');
  }
});

// Change deep property, effect runs automatically
app.currentUser.preferences.notifications.email = false;

Key Takeaway

Deep reactivity allows you to treat complex, nested data as a single reactive unit. You don't need to think about "making things reactive" at each level — you just work with your data naturally, and the reactive system tracks everything automatically, no matter how deep.

Adding New Properties

New properties added to reactive objects are also reactive:

js
const user = state({
  name: 'John'
});

// Add a new property
user.email = 'john@example.com';

// Add a new nested object
user.profile = {
  bio: 'Developer',
  avatar: 'avatar.jpg'
};

// The new properties are automatically reactive
effect(() => {
  console.log('Bio: ' + user.profile.bio);
});

user.profile.bio = 'Senior Developer'; // Effect re-runs!

Working with Arrays

Arrays inside a reactive state work just like normal JavaScript arrays — but they are fully reactive.

That means any change to the array is automatically detected and will trigger updates.

Example: Reactive Array

javascript
const todos = state({
  items: ['Task 1', 'Task 2', 'Task 3']
});

effect(() => {
  console.log('Todo count: ' + todos.items.length);
  console.log('Todos: ' + todos.items.join(', '));
});

Here's what's happening:

  • The effect() reads todos.items.length
  • It also reads the contents of todos.items
  • These reads are automatically tracked

So the effect now depends on the array.

Mutating the Array (The Important Part)

When you modify the array, the Proxy detects it and re-runs the effect.

javascript
todos.items.push('Task 4');   // Effect re-runs
todos.items.pop();            // Effect re-runs
todos.items[0] = 'Updated';   // Effect re-runs
todos.items.splice(1, 1);     // Effect re-runs

You don't need special methods. You don't need to replace the array.

You use normal JavaScript array operations.

Why This Works

Behind the scenes, the array is wrapped in a Proxy, just like objects.

The Proxy watches for:

  • Changes to the array length
  • Index updates (items[0] = ...)
  • Mutating methods (push, pop, splice, etc.)

Whenever one of these happens:

  1. The Proxy detects the change
  2. It finds all effects that depend on the array
  3. Those effects automatically re-run

Reactive Array Methods

Reactive arrays support all standard JavaScript array methods.

The important thing to understand is which methods trigger updates and why.

Mutating Methods (Trigger Updates)

These methods change the array itself. Because the array changes, the reactive system detects it and updates automatically.

javascript
const list = state({
  numbers: [1, 2, 3, 4, 5]
});

list.numbers.push(6);
list.numbers.pop();
list.numbers.shift();
list.numbers.unshift(0);
list.numbers.splice(2, 1);
list.numbers.sort();
list.numbers.reverse();

✔ The array is modified
✔ The Proxy detects the change
✔ Effects and DOM updates re-run

Non-Mutating Methods (No Direct Updates)

These methods do not change the original array. They return a new array instead.

javascript
const filtered = list.numbers.filter(n => n > 2);
const mapped = list.numbers.map(n => n * 2);
const found = list.numbers.find(n => n === 3);

Important to note:

  • The original reactive array is not modified
  • No updates are triggered by these calls alone
  • The returned arrays are plain JavaScript arrays

Making Non-Mutating Results Reactive

If you want the result to be reactive, you must store it in reactive state:

javascript
list.numbers = list.numbers.filter(n => n > 2);

Now:

  • The array is replaced
  • The Proxy detects the change
  • Updates are triggered

Key Takeaway

Reactivity is triggered by changes, not by method names.

  • Methods that mutate the array → trigger updates
  • Methods that return new arrays → do nothing unless assigned back
  • Everything follows normal JavaScript rules

No special API. No surprises. Just predictable, reactive behavior.

Understanding Why filter() Needs Assignment

That's a very good confusion — and it's a place where many beginners (and even experienced devs) get stuck. Let's slow it down and rebuild the idea from zero, step by step.

The Core Idea (One Sentence)

Reactivity only happens when the reactive state itself changes.

Keep this sentence in mind — everything below explains why.

Step 1: What filter() Actually Does (Plain JavaScript)

This is very important:

js
list.numbers.filter(n => n > 2);

filter() does NOT change the array.

It:

  • looks at the array
  • creates a new array
  • returns it

Example:

js
const a = [1, 2, 3, 4, 5];
const b = a.filter(n => n > 2);

console.log(a); // [1, 2, 3, 4, 5]  (unchanged)
console.log(b); // [3, 4]        (new array)

So after this runs:

  • list.numbers is still the same array
  • Reactivity sees no change

Step 2: Why No Reactive Update Happens

js
const filtered = list.numbers.filter(n => n > 2);

What the reactive system sees:

  • ✔ You read list.numbers
  • ❌ You did not change list.numbers

Reading does not trigger updates. Only writing does.

So:

  • effects do NOT re-run
  • DOM does NOT update

This is correct and expected.

Step 3: What This Line Actually Means

Now look at this line again:

js
list.numbers = list.numbers.filter(n => n > 2);

Break it into two steps:

1️⃣ Right side runs first

js
list.numbers.filter(n => n > 2);
// returns a NEW array

Example result:

js
[3, 4, 5] // The new array return by filter()

2️⃣ Left side assigns it back

js
list.numbers = [3, 4, 5];

⚠️ THIS is the key moment

You are:

  • replacing the old array => [1, 2, 3, 4, 5]
  • with a new array => [3, 4, 5]
  • inside reactive state

✅ That is a write ✅ The Proxy detects it ✅ Updates are triggered

Step 4: Why This Triggers Reactivity

Because from the system's point of view, this happened:

js
// BEFORE
list.numbers === [1, 2, 3, 4, 5]

// AFTER
list.numbers === [3, 4, 5]

That is a real state change.

So:

  • effects re-run
  • UI updates
  • everything stays in sync

Mental Model (Very Important)

❌ This does NOT change state

js
list.numbers.filter(...)

✅ This DOES change state

js
list.numbers = somethingNew

Reactivity cares about assignment, not about which method you used.

Simple Rule to Remember

Non-mutating methods must be assigned back to reactive state to trigger updates.

Simple Example: Reactive Array Update

js
const app = state({
  items: ['A', 'B', 'C']
});

effect(() => {
  console.log(app.items.join(', '));
});

Mutating the Array (Works Automatically)

js
app.items.push('D');
// Logs: A, B, C, D

Why?

  • The array is changed
  • Reactivity detects it
  • Effect re-runs

Non-Mutating Method (No Update)

js
app.items.filter(item => item !== 'B');
// Nothing happens

Why?

  • filter() returns a new array
  • app.items was not changed

Making It Reactive (Correct Way)

js
app.items = app.items.filter(item => item !== 'B');
// Logs: A, C, D

Why?

  • The array is replaced
  • Reactive state changes
  • Effect re-runs

One-Line Rule

If the array changes, updates happen. If you don't assign it back, nothing changes.

Summary

Mental Model: Think of state() as a smart home system - it watches for changes and automatically triggers reactions throughout your application.

Remember: state() is the foundation of reactivity. It makes your data "smart" so it can automatically trigger updates when values change. Combined with effect(), it creates truly reactive applications!