Understanding ref() - A Beginner's Guide
Quick Start (30 seconds)
Need to make a single value reactive? Here's how:
// Create a reactive reference to a single value
const count = ref(0);
// Automatically update UI when it changes
effect(() => {
document.getElementById('display').textContent = count.value;
});
// Update the value - UI updates automatically!
count.value = 5; // Display shows "5"
count.value++; // Display shows "6"That's it! The ref() function wraps a single value in a reactive container. Access it with .value, and changes automatically trigger updates.
What is ref()?
ref() is a specialized function for creating reactive primitive values. While state() is designed for objects, ref() is perfect for single values like numbers, strings, or booleans.
A reactive ref:
- Wraps a single value in a reactive container
- Detects when that value is read (via
.value) - Detects when that value is changed (via
.value = ...) - Automatically notifies effects and watchers
Think of it as upgrading a simple variable to a smart variable - one that can automatically trigger updates throughout your application when it changes.
Syntax
// Using the shortcut
ref(initialValue)
// Using the full namespace
ReactiveUtils.ref(initialValue)Both styles are valid! Choose whichever you prefer:
- Shortcut style (
ref()) - Clean and concise - Namespace style (
ReactiveUtils.ref()) - Explicit and clear
Parameters:
initialValue- The starting value (can be any type: number, string, boolean, null, etc.)
Returns:
- A reactive object with a
.valueproperty that holds the actual value
Why Does This Exist?
The Problem with Regular Variables
Let's say you have a simple variable:
// Regular variable - no special powers
let count = 0;
// You can read the value
console.log(count); // 0
// You can update the value
count = 5; // Changed, but nobody knows!
console.log(count); // 5At 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 Variable Change Flow:
┌─────────────┐
│ count = 5 │
└──────┬──────┘
│
▼
[SILENCE]
│
▼
Nothing happens
No notifications
No UI updates
No side effectsProblems:
- When you change
count, nothing else in your code knows about it - 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 the value changes
- You have to manually sync changes everywhere
- The variable changes, but nothing reacts to it
Why This Becomes a Problem:
❌ 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 variables have no awareness of change. They store data — but they don't communicate.
The Problem with Using state() for Single Values
You might think: "Why not just use state() for everything?"
// Using state() for a single value (awkward)
const countState = state({ value: 0 });
// You have to wrap it in an object
console.log(countState.value); // Access the value
countState.value = 5; // Update the valueThis works, but it's unnecessarily verbose for simple values. You're creating a whole object just to hold one value.
Problems: ❌ Extra boilerplate for simple cases ❌ Awkward syntax for single values ❌ Not clear at a glance that it's just one value ❌ More typing for common scenarios
The Solution with ref()
When you use ref(), you get a reactive container designed specifically for single values:
// Reactive reference - clean and purpose-built! ✨
const count = ref(0);
// You can now attach logic that automatically runs when it changes
effect(() => {
console.log('Count is: ' + count.value);
});
// Immediately logs: "Count is: 0"
// Now change the value
count.value = 5;
// Automatically logs: "Count is: 5"What Just Happened?
Reactive Ref Change Flow:
┌─────────────┐
│ count.value │
│ = 5 │
└──────┬──────┘
│
▼
[PROXY DETECTS]
│
▼
Notifies all
watching effects
│
▼
✅ UI updates
✅ Side effects run
✅ Computed values refreshWith ref():
- Changes are detected automatically
- Any code that depends on the value re-runs by itself
- You don't need to manually track or trigger updates
- The UI stays in sync with your data
- Clean, simple syntax for single values
Benefits: ✅ Changes are automatically detected ✅ Code can automatically respond to changes ✅ Perfect for primitive values (numbers, strings, booleans) ✅ Less boilerplate than wrapping in state() ✅ Clear intent: this is a single reactive value
Mental Model
Think of ref() like a smart display case:
Regular Variable (Unlabeled Box):
┌──────────────┐
│ value: 5 │ ← You can see it
└──────────────┘
No sensors
No notifications
No automation
Reactive Ref (Smart Display Case):
┌──────────────────┐
│ .value: 5 │ ←─── Sensors watching
│ │
│ ┌────────────┐ │
│ │ Actual: 5 │ │
│ └────────────┘ │
└──────────────────┘
│
▼
┌─────────────┐
│ Controller │
└──────┬──────┘
│
▼
When .value changes:
✓ UI updates
✓ Effects re-run
✓ Watchers notifyKey Insight: Just like a smart display case that monitors what's inside and alerts you when the contents change, a ref() automatically tracks when its .value is accessed or modified, triggering all connected reactions.
How Does It Work?
The Magic: Reactive Wrapper Around State
When you call ref(), here's what happens behind the scenes:
// What you write:
const count = ref(0);
// What actually happens (simplified):
const count = state({ value: 0 });
// Plus special methods:
count.valueOf = function() { return this.value; };
count.toString = function() { return String(this.value); };In other words: ref() is actually a convenience wrapper that:
- Creates a
state()object with a singlevalueproperty - Adds special methods for easier use in expressions
- Returns the reactive object
Under the Hood
ref(0)
│
▼
┌───────────────────────┐
│ Creates state({}) │
│ with .value prop │
└──────────┬────────────┘
│
▼
┌───────────────────────┐
│ Proxy Layer │
├───────────────────────┤
│ GET .value ───► Track dependency
│ SET .value ───► Trigger effects
└───────────────────────┘
│
▼
┌───────────────────────┐
│ Actual Value: 0 │
└───────────────────────┘What happens:
1️⃣ When you read count.value, the Proxy notices and tracks it 2️⃣ When you write count.value = 5, the Proxy notices and triggers updates 3️⃣ Any code that depends on count.value automatically re-runs
This is completely transparent - you use .value to access the data, and the reactive system handles everything else!
Basic Usage
Creating Reactive Refs
The simplest way to use ref() is to wrap a primitive value:
// Using the shortcut style
const message = ref('Hello');
const count = ref(0);
const isActive = ref(true);
const data = ref(null);
// Or using the namespace style
const message = ReactiveUtils.ref('Hello');
const count = ReactiveUtils.ref(0);
const isActive = ReactiveUtils.ref(true);That's it! Now each variable is reactive - it can detect and respond to changes.
Accessing the Value
To get or set the value, use the .value property:
const count = ref(0);
// Read the value
console.log(count.value); // Output: 0
// Update the value
count.value = 5;
console.log(count.value); // Output: 5
// Use in expressions
const doubled = count.value * 2;
console.log(doubled); // Output: 10Important: You must use .value to access the actual data. The ref itself is a reactive container.
Understanding .value
What Does ".value" Mean?
This is a very important concept. Let's break it down step by step.
When you create a ref:
const count = ref(0);count is not the number 0. It's a reactive container that holds the number 0.
count = {
value: 0, ← The actual data is here
valueOf: function() { ... },
toString: function() { ... },
watch: function() { ... },
// ... other reactive methods
}Why Not Just Use the Variable Directly?
That's a very good question — and it's a place where many beginners get confused. Let's understand why.
The Core Problem:
In JavaScript, when you assign a primitive value (like a number), you're just copying the value:
let x = 5;
let y = x; // y gets a copy of 5
x = 10; // x changes to 10, but y is still 5Primitives are immutable and copied by value. There's no way to "watch" a primitive variable for changes.
Why .value is Necessary:
By wrapping the value in an object, we can:
- Keep a reference to the container (the ref object)
- Use a Proxy to watch when
.valueis accessed or changed - Trigger effects when
.valuechanges
// Without .value (impossible to track):
let count = 0;
count = 5; // How would we know this happened?
// With .value (trackable):
const count = ref(0);
count.value = 5; // The Proxy sees: "Someone changed .value!"Reading .value
Every time you access .value, you're:
- Reading the current data
- Automatically registering a dependency (if inside an effect)
const count = ref(0);
// Inside an effect, reading .value tracks the dependency
effect(() => {
console.log(count.value); // ← This registers: "effect depends on count"
});
// Now this will trigger the effect
count.value = 5; // Effect re-runs, logs: 5Writing .value
Every time you write to .value, you're:
- Updating the stored data
- Automatically triggering all effects that depend on it
const count = ref(0);
effect(() => {
document.getElementById('display').textContent = count.value;
});
// This triggers the effect
count.value = 10; // Display updates to "10"One-Line Rule
To access or change the data inside a ref, always use .value
Using Refs with Effects
Refs become truly powerful when combined with effects:
const count = ref(0);
// Effect runs whenever count.value changes
effect(() => {
console.log('Count is now: ' + count.value);
document.getElementById('display').textContent = count.value;
});
// Immediately logs: "Count is now: 0"
// And updates the DOMWhat's happening:
1️⃣ The effect runs immediately 2️⃣ It reads count.value 3️⃣ The reactive system tracks: "This effect depends on count.value" 4️⃣ Whenever count.value changes, the effect automatically re-runs
Multiple Effects
You can have multiple effects watching the same ref:
const temperature = ref(20);
// Effect #1: Update display
effect(() => {
document.getElementById('temp').textContent = temperature.value + '°C';
});
// Effect #2: Check if too hot
effect(() => {
const isTooHot = temperature.value > 30;
console.log('Too hot:', isTooHot);
});
// Effect #3: Change background color
effect(() => {
const color = temperature.value > 25 ? 'red' : 'blue';
document.body.style.backgroundColor = color;
});
// One change triggers all three effects
temperature.value = 35;
// Display updates to "35°C"
// Console logs: "Too hot: true"
// Background turns redSpecial Ref Features
valueOf() and toString()
Refs include special methods that make them easier to use in expressions:
const count = ref(5);
// valueOf() - returns the numeric value
console.log(count.valueOf()); // 5
console.log(count + 10); // 15 (uses valueOf automatically)
// toString() - returns string representation
console.log(count.toString()); // "5"
console.log('Count: ' + count); // "Count: 5" (uses toString automatically)Why This Matters:
Without these methods, you'd always need .value:
// Without valueOf/toString (tedious):
console.log(count.value + 10);
console.log('Count: ' + count.value);
// With valueOf/toString (convenient):
console.log(count + 10); // Works!
console.log('Count: ' + count); // Works!However, for clarity and consistency, it's still recommended to use .value explicitly in most cases.
Instance Methods
Since ref() returns a reactive state object, you get all the standard state methods:
const count = ref(0);
// Watch for changes
count.watch('value', (newVal, oldVal) => {
console.log(`Changed from ${oldVal} to ${newVal}`);
});
// Batch multiple updates
batch(() => {
count.value++;
count.value++;
count.value++;
});
// Only triggers effects once, not three times
// Get raw (non-reactive) value
const raw = toRaw(count);
console.log(raw); // { value: 3 }Refs vs State Objects
When to Use ref()
Use ref() when you need a single reactive value:
✅ Counters, toggles, flags ✅ Single strings or numbers ✅ Loading states, error messages ✅ Any primitive value that changes over time
const count = ref(0);
const message = ref('Hello');
const isLoading = ref(false);
const userId = ref(null);When to Use state()
Use state() when you need multiple related values in an object:
✅ User profiles, settings, configurations ✅ Form data with multiple fields ✅ Complex objects with nested properties
const user = state({
name: 'John',
age: 25,
email: 'john@example.com'
});Quick Comparison
// ❌ Using state() for a single value (verbose)
const countState = state({ value: 0 });
countState.value = 5;
// ✅ Using ref() for a single value (clean)
const count = ref(0);
count.value = 5;
// ❌ Using ref() for multiple values (awkward)
const nameRef = ref('John');
const ageRef = ref(25);
const emailRef = ref('john@example.com');
// ✅ Using state() for multiple values (organized)
const user = state({
name: 'John',
age: 25,
email: 'john@example.com'
});Simple Rule:
- One value? Use
ref() - Multiple related values? Use
state()
Working with Multiple Refs
Creating Many Refs Manually
const firstName = ref('John');
const lastName = ref('Doe');
const age = ref(25);
const email = ref('john@example.com');
// Use them independently
effect(() => {
console.log(`${firstName.value} ${lastName.value}`);
});
firstName.value = 'Jane'; // Only this effect rerunsUsing refs() for Bulk Creation
If you need to create multiple refs at once, use the refs() helper:
// Create multiple refs in one call
const { count, message, isActive } = refs({
count: 0,
message: 'Hello',
isActive: true
});
// Each is a separate ref
console.log(count.value); // 0
console.log(message.value); // "Hello"
console.log(isActive.value); // true
// Update independently
count.value = 5;
message.value = 'Goodbye';What refs() does:
// This:
const { count, message } = refs({ count: 0, message: 'Hi' });
// Is equivalent to:
const count = ref(0);
const message = ref('Hi');It's just a convenience function that creates multiple refs and returns them as an object.
Common Patterns
Counter Pattern
const count = ref(0);
// Increment button
document.getElementById('increment').onclick = () => {
count.value++;
};
// Decrement button
document.getElementById('decrement').onclick = () => {
count.value--;
};
// Auto-update display
effect(() => {
document.getElementById('display').textContent = count.value;
});Toggle Pattern
const isOpen = ref(false);
// Toggle function
function toggle() {
isOpen.value = !isOpen.value;
}
// Auto-update UI based on state
effect(() => {
const menu = document.getElementById('menu');
menu.style.display = isOpen.value ? 'block' : 'none';
});Loading State Pattern
const isLoading = ref(false);
const data = ref(null);
const error = ref(null);
async function fetchData() {
isLoading.value = true;
error.value = null;
try {
const response = await fetch('/api/data');
data.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
isLoading.value = false;
}
}
// Auto-update UI
effect(() => {
if (isLoading.value) {
document.getElementById('status').textContent = 'Loading...';
} else if (error.value) {
document.getElementById('status').textContent = 'Error: ' + error.value;
} else if (data.value) {
document.getElementById('status').textContent = 'Success!';
}
});Input Binding Pattern
const inputValue = ref('');
const input = document.getElementById('myInput');
// Update ref when input changes
input.addEventListener('input', (e) => {
inputValue.value = e.target.value;
});
// Update display when ref changes
effect(() => {
document.getElementById('display').textContent = inputValue.value;
});Computed from Ref Pattern
const radius = ref(5);
// Create a state object with computed area
const circle = state({});
circle.computed('area', function() {
return Math.PI * radius.value * radius.value;
});
effect(() => {
console.log(`Radius: ${radius.value}, Area: ${circle.area}`);
});
radius.value = 10; // Effect re-runs with new areaCommon Pitfalls
Pitfall #1: Forgetting .value
❌ Wrong:
const count = ref(0);
count = 5; // ERROR: Reassigning the ref itself!✅ Correct:
const count = ref(0);
count.value = 5; // Updates the value inside the refWhat's happening:
countis the reactive containercount.valueis the actual data- You must access
.valueto get or set the data
Pitfall #2: Using Refs in Template Literals (Outside Effects)
❌ Wrong:
const name = ref('John');
const greeting = `Hello, ${name.value}!`;
name.value = 'Jane';
console.log(greeting); // Still "Hello, John!" - not reactiveThe problem: String templates are evaluated once. They don't re-run when refs change.
✅ Correct (with effect):
const name = ref('John');
effect(() => {
const greeting = `Hello, ${name.value}!`;
console.log(greeting);
});
name.value = 'Jane'; // Effect re-runs, logs "Hello, Jane!"Or use a computed property:
const name = ref('John');
const greetingState = state({});
greetingState.computed('greeting', function() {
return `Hello, ${name.value}!`;
});
effect(() => {
console.log(greetingState.greeting);
});
name.value = 'Jane'; // Computed updates, effect re-runsPitfall #3: Destructuring Loses Reactivity
❌ Wrong:
const count = ref(0);
const { value } = count; // Extracts the value (now it's just 0)
console.log(value); // 0
count.value = 5;
console.log(value); // Still 0 - not reactive!What happened: Destructuring extracts the current value. It's no longer connected to the ref.
✅ Correct:
const count = ref(0);
// Always access through the ref
console.log(count.value); // 0
count.value = 5;
console.log(count.value); // 5 - stays reactivePitfall #4: Comparing Refs Directly
❌ Wrong:
const count1 = ref(5);
const count2 = ref(5);
if (count1 === count2) { // Comparing ref objects, not values
console.log('Equal');
}
// This will be FALSE - they're different objects✅ Correct:
const count1 = ref(5);
const count2 = ref(5);
if (count1.value === count2.value) { // Compare the values
console.log('Equal');
}
// This will be TRUEPitfall #5: Passing Refs to Functions
When you pass a ref to a function, remember to access .value:
❌ Wrong:
const count = ref(10);
function double(num) {
return num * 2;
}
console.log(double(count)); // NaN or [object Object]2✅ Correct:
const count = ref(10);
function double(num) {
return num * 2;
}
console.log(double(count.value)); // 20Alternative (if function expects a ref):
function doubleRef(refObj) {
return refObj.value * 2;
}
console.log(doubleRef(count)); // 20Summary
What is ref()?
ref() creates a reactive container for a single value. It's perfect for primitive values like numbers, strings, and booleans.
Why use ref() instead of state()?
- Cleaner syntax for single values
- Less boilerplate
- Clear intent: "this is one reactive value"
- Purpose-built for primitives
Key Points to Remember:
1️⃣ Always use .value to access or modify the data 2️⃣ Use ref() for single values, state() for objects 3️⃣ Refs work with effects - changes trigger automatic re-runs 4️⃣ Don't destructure refs - you'll lose reactivity 5️⃣ Compare .value, not the ref itself
Mental Model: Think of ref() as a smart display case - it holds a single value and automatically notifies your app when that value changes. You access the value through the glass (.value), and the case monitors all access and changes.
Quick Reference:
// Create
const count = ref(0);
// Read
console.log(count.value);
// Write
count.value = 5;
// Use in effect
effect(() => {
console.log(count.value);
});
// Watch changes
count.watch('value', (newVal, oldVal) => {
console.log(`Changed from ${oldVal} to ${newVal}`);
});
// Multiple refs
const { x, y, z } = refs({ x: 0, y: 0, z: 0 });Remember: ref() is your go-to tool for making single values reactive. Combined with effect(), it creates truly reactive applications where your UI automatically stays in sync with your data! 🎉