Skip to content

Sponsor

Sponsor

Understanding ref() - A Beginner's Guide

Quick Start (30 seconds)

Need to make a single value reactive? Here's how:

js
// 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

js
// 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 .value property that holds the actual value

Why Does This Exist?

The Problem with Regular Variables

Let's say you have a simple variable:

javascript
// 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); // 5

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 Variable Change Flow:
┌─────────────┐
│  count = 5  │
└──────┬──────┘


   [SILENCE]


 Nothing happens
 No notifications
 No UI updates
 No side effects

Problems:

  • 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?"

javascript
// 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 value

This 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:

javascript
// 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 refresh

With 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 notify

Key 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:

javascript
// 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:

  1. Creates a state() object with a single value property
  2. Adds special methods for easier use in expressions
  3. 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:

js
// 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:

js
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: 10

Important: 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:

js
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:

js
let x = 5;
let y = x;  // y gets a copy of 5
x = 10;     // x changes to 10, but y is still 5

Primitives 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:

  1. Keep a reference to the container (the ref object)
  2. Use a Proxy to watch when .value is accessed or changed
  3. Trigger effects when .value changes
js
// 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:

  1. Reading the current data
  2. Automatically registering a dependency (if inside an effect)
js
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: 5

Writing .value

Every time you write to .value, you're:

  1. Updating the stored data
  2. Automatically triggering all effects that depend on it
js
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:

javascript
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 DOM

What'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:

javascript
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 red

Special Ref Features

valueOf() and toString()

Refs include special methods that make them easier to use in expressions:

js
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:

js
// 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:

js
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

js
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

js
const user = state({
  name: 'John',
  age: 25,
  email: 'john@example.com'
});

Quick Comparison

javascript
// ❌ 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

js
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 reruns

Using refs() for Bulk Creation

If you need to create multiple refs at once, use the refs() helper:

js
// 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:

javascript
// 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

js
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

js
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

js
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

js
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

js
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 area

Common Pitfalls

Pitfall #1: Forgetting .value

Wrong:

js
const count = ref(0);
count = 5; // ERROR: Reassigning the ref itself!

Correct:

js
const count = ref(0);
count.value = 5; // Updates the value inside the ref

What's happening:

  • count is the reactive container
  • count.value is the actual data
  • You must access .value to get or set the data

Pitfall #2: Using Refs in Template Literals (Outside Effects)

Wrong:

js
const name = ref('John');
const greeting = `Hello, ${name.value}!`;

name.value = 'Jane';
console.log(greeting); // Still "Hello, John!" - not reactive

The problem: String templates are evaluated once. They don't re-run when refs change.

Correct (with effect):

js
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:

js
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-runs

Pitfall #3: Destructuring Loses Reactivity

Wrong:

js
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:

js
const count = ref(0);

// Always access through the ref
console.log(count.value); // 0

count.value = 5;
console.log(count.value); // 5 - stays reactive

Pitfall #4: Comparing Refs Directly

Wrong:

js
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:

js
const count1 = ref(5);
const count2 = ref(5);

if (count1.value === count2.value) {  // Compare the values
  console.log('Equal');
}
// This will be TRUE

Pitfall #5: Passing Refs to Functions

When you pass a ref to a function, remember to access .value:

Wrong:

js
const count = ref(10);

function double(num) {
  return num * 2;
}

console.log(double(count)); // NaN or [object Object]2

Correct:

js
const count = ref(10);

function double(num) {
  return num * 2;
}

console.log(double(count.value)); // 20

Alternative (if function expects a ref):

js
function doubleRef(refObj) {
  return refObj.value * 2;
}

console.log(doubleRef(count)); // 20

Summary

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:

js
// 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! 🎉