Understanding state() - A Beginner's Guide
Quick Start (30 seconds)
Want to make your JavaScript objects reactive? Here's how:
// 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
// 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:
// 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 effectsProblems:
- 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:
// 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 refreshWith 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 recordingKey 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 writesYou 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:
- When you read a property (
user.name), the Proxy notices and tracks it - When you write a property (
user.name = 'Jane'), the Proxy notices and triggers updates - 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:
// 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:
// 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.namemeans: "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:
// 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: trueThe 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:
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.95Key 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:
object.propertyName // That's it!No special functions needed to read values. Just use the dot notation like always! 🎉
Visual Comparison
// 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 =:
// 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: 30That'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:
// 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: falseNo 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
myState.count = 10; // Set to a specific value
myState.message = 'Goodbye'; // Replace textIncrement & Decrement
myState.count++; // Increase by 1 (same as: count = count + 1)
myState.count--; // Decrease by 1 (same as: count = count - 1)Arithmetic Operations
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 * 2String Operations
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
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
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
// 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:
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
// ❌ 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:
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.
// 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.
// Changes automatically trigger the effect
counter.count = 5; // Effect runs, updates DOM
counter.count = 10; // Effect runs againWhen 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 subscribersHere's a more concrete example with multiple effects:
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:
- Proxy intercepts: "Someone is writing to
counter.count" - System looks up: "I have 3 effects watching
counter.count" - Notification cascade: All three effects re-run automatically
- 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.
const app = state({
user: {
name: 'John',
address: {
city: 'New York',
country: 'USA'
}
}
});What matters here is what gets read inside the effect:
app.userapp.user.addressapp.user.address.city
Each read is automatically tracked by the reactive system. The effect is now subscribed specifically to address.city.
effect(() => {
console.log('City: ' + app.user.address.city);
});Deep changes are tracked! Even though this change happens several levels deep:
- The proxy detects the write
- The system finds all effects that depend on
address.city - Those effects re-run automatically
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():
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):
// You have to wrap each level
const app = state({
user: state({
profile: state({
settings: state({
theme: 'dark'
})
})
})
});With deep reactivity (automatic):
// 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 effectsBenefits
- ✅ 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:
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:
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
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()readstodos.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.
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-runsYou 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:
- The Proxy detects the change
- It finds all effects that depend on the array
- 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.
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.
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:
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:
list.numbers.filter(n => n > 2);filter() does NOT change the array.
It:
- looks at the array
- creates a new array
- returns it
Example:
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.numbersis still the same array- Reactivity sees no change
Step 2: Why No Reactive Update Happens
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:
list.numbers = list.numbers.filter(n => n > 2);Break it into two steps:
1️⃣ Right side runs first
list.numbers.filter(n => n > 2);
// returns a NEW arrayExample result:
[3, 4, 5] // The new array return by filter()2️⃣ Left side assigns it back
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:
// 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
list.numbers.filter(...)✅ This DOES change state
list.numbers = somethingNewReactivity 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
const app = state({
items: ['A', 'B', 'C']
});
effect(() => {
console.log(app.items.join(', '));
});Mutating the Array (Works Automatically)
app.items.push('D');
// Logs: A, B, C, DWhy?
- The array is changed
- Reactivity detects it
- Effect re-runs
Non-Mutating Method (No Update)
app.items.filter(item => item !== 'B');
// Nothing happensWhy?
filter()returns a new arrayapp.itemswas not changed
Making It Reactive (Correct Way)
app.items = app.items.filter(item => item !== 'B');
// Logs: A, C, DWhy?
- 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!