Skip to content

Sponsor

Sponsor

Understanding array.push() in Reactive Arrays - A Beginner's Guide

Table of Contents

Quick Start (30 seconds)

Need to add items to the end of a reactive array? Just use push():

js
const app = state({
  todos: ['Buy milk', 'Walk dog']
});

// Set up a watcher
effect(() => {
  console.log('Todo count:', app.todos.length);
});
// Logs: "Todo count: 2"

// Add new item
app.todos.push('Call mom');
// Logs: "Todo count: 3" (reactivity triggered!)

// Add multiple items
app.todos.push('Read book', 'Exercise');
// Logs: "Todo count: 5"

That's it! push() adds items to the end of reactive arrays and automatically triggers updates!

What is Reactive push()?

The reactive push() method is an enhanced version of the standard array push() method that automatically triggers reactive updates when items are added to the end of an array.

This method:

  • Adds one or more items to the end of an array
  • Returns the new length of the array
  • Automatically triggers reactive effects, watchers, and bindings
  • Works exactly like standard Array.prototype.push()
  • Is available on all reactive array properties

Think of it as push() with superpowers - it does everything the normal push() does, but also notifies your reactive system that the array changed.

Syntax

js
// Add single item
array.push(item)

// Add multiple items
array.push(item1, item2, item3)

// Returns new length
const newLength = array.push(item)

// Full examples
const app = state({
  items: ['A', 'B']
});

app.items.push('C');           // Returns 3
app.items.push('D', 'E', 'F'); // Returns 6

Parameters:

  • ...items - One or more items to add to the end of the array

Returns:

  • number - The new length of the array

Why Does This Exist?

The Real Issue

In standard JavaScript, array mutation methods don't notify anyone when they change the array:

js
const items = ['A', 'B'];

items.push('C'); // Array changed, but no one knows!
// UI doesn't update, effects don't run

What's the Real Issue?

STANDARD ARRAY MUTATION (No Reactivity):
┌─────────────────────────────────────────────────┐
│                                                 │
│  items = ['A', 'B']                            │
│      ↓                                          │
│  items.push('C')  ← Mutation happens           │
│      ↓                                          │
│  items = ['A', 'B', 'C']                       │
│                                                 │
│  ❌ Effects don't run                          │
│  ❌ Watchers don't trigger                     │
│  ❌ UI doesn't update                          │
│                                                 │
└─────────────────────────────────────────────────┘

REACTIVE ARRAY MUTATION (With Reactivity):
┌─────────────────────────────────────────────────┐
│                                                 │
│  items = ['A', 'B']  (reactive)                │
│      ↓                                          │
│  items.push('C')  ← Patched method              │
│      ↓                                          │
│  [Reactive system notified!]                    │
│      ↓                                          │
│  ✅ Effects re-run automatically               │
│  ✅ Watchers triggered                         │
│  ✅ UI updates automatically                   │
│                                                 │
└─────────────────────────────────────────────────┘

The Solution

The Reactive system patches the push() method on reactive arrays so that:

  1. The normal push() behavior happens (items added to end)
  2. The reactive system is notified of the change
  3. All effects, watchers, and bindings automatically update

You use push() exactly as you normally would - the reactivity happens automatically!

Mental Model

Think of reactive push() like adding items to a shopping cart with automatic inventory updates:

Standard Array (Manual Process):

You add items to cart
→ Items are added
→ You manually update the cart count
→ You manually refresh the display
→ You manually notify shipping

Reactive Array (Automatic Process):

You add items to cart
→ Items are added
→ Cart count updates automatically
→ Display refreshes automatically  
→ Shipping notified automatically
→ Total price recalculates automatically

The reactive push() handles all the "notification work" for you - you just add items and everything else updates automatically!

How Does It Work?

Under the hood, reactive push() works by wrapping the native array method:

js
// Simplified implementation
function patchPush(array, state, key) {
  const originalPush = Array.prototype.push;
  
  array.push = function(...items) {
    // 1. Call the original push method
    const result = originalPush.apply(this, items);
    
    // 2. Notify the reactive system
    const updatedArray = [...this];
    state[key] = updatedArray; // Triggers reactivity!
    
    // 3. Return the new length (like normal push)
    return result;
  };
}

The process:

  1. You call push() on a reactive array
  2. Original behavior happens - Items added to end
  3. Reactive notification - System detects the change
  4. Effects re-run - Anything watching the array updates
  5. Returns new length - Just like standard push()

All of this happens automatically when you use reactive arrays created with state(), reactive(), or after calling ReactiveUtils.patchArray().

Basic Usage

Adding Single Items

js
const app = state({
  messages: []
});

// Add messages one at a time
app.messages.push('Hello');
app.messages.push('World');
app.messages.push('!');

console.log(app.messages);
// ['Hello', 'World', '!']

Adding Multiple Items

js
const app = state({
  tags: ['javascript']
});

// Add multiple tags at once
app.tags.push('react', 'vue', 'angular');

console.log(app.tags);
// ['javascript', 'react', 'vue', 'angular']

Using Return Value

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

const newLength = app.items.push('C', 'D');
console.log(newLength);  // 4
console.log(app.items);  // ['A', 'B', 'C', 'D']

With Effects

js
const app = state({
  todos: []
});

effect(() => {
  console.log(`You have ${app.todos.length} todos`);
});
// Logs: "You have 0 todos"

app.todos.push('Buy milk');
// Logs: "You have 1 todos"

app.todos.push('Walk dog', 'Call mom');
// Logs: "You have 3 todos"

Advanced Usage

Real-time Chat Messages

js
const chat = state({
  messages: []
});

// Update UI automatically when messages arrive
effect(() => {
  const messageList = document.querySelector('#messages');
  messageList.innerHTML = chat.messages
    .map(msg => `<div>${msg.text}</div>`)
    .join('');
});

// Simulate receiving messages
function receiveMessage(text) {
  chat.messages.push({
    id: Date.now(),
    text,
    timestamp: new Date()
  });
  // UI updates automatically!
}

receiveMessage('Hello!');
receiveMessage('How are you?');

Live Activity Feed

js
const feed = state({
  activities: []
});

// Show latest 5 activities
const latestActivities = computed(() => {
  return feed.activities.slice(-5).reverse();
});

effect(() => {
  console.log('Recent activities:', latestActivities.value);
});

// Add activities
feed.activities.push({ user: 'Alice', action: 'logged in' });
feed.activities.push({ user: 'Bob', action: 'posted a comment' });
feed.activities.push({ user: 'Charlie', action: 'liked a post' });
// Computed property recalculates, effect re-runs

Form Validation with Error Collection

js
const form = state({
  errors: []
});

function validateForm(data) {
  form.errors = []; // Clear previous errors
  
  if (!data.email) {
    form.errors.push('Email is required');
  }
  
  if (!data.password) {
    form.errors.push('Password is required');
  }
  
  if (data.password && data.password.length < 8) {
    form.errors.push('Password must be at least 8 characters');
  }
  
  return form.errors.length === 0;
}

effect(() => {
  const errorDiv = document.querySelector('#errors');
  if (form.errors.length > 0) {
    errorDiv.innerHTML = form.errors
      .map(err => `<p class="error">${err}</p>`)
      .join('');
  } else {
    errorDiv.innerHTML = '';
  }
});

validateForm({ email: '', password: '123' });
// Errors display automatically

Building Lists from API Responses

js
const app = state({
  users: []
});

async function loadMoreUsers(page) {
  const response = await fetch(`/api/users?page=${page}`);
  const newUsers = await response.json();
  
  // Add new users to existing list
  newUsers.forEach(user => {
    app.users.push(user);
  });
  // UI updates automatically with all users
}

effect(() => {
  console.log(`Showing ${app.users.length} users`);
});

loadMoreUsers(1); // Load page 1
loadMoreUsers(2); // Load page 2 and append

Common Patterns

1. Adding Items to a List

js
const app = state({
  items: []
});

function addItem(item) {
  app.items.push(item);
}

addItem({ id: 1, name: 'Apple' });
addItem({ id: 2, name: 'Banana' });

2. Building Arrays Incrementally

js
const data = state({
  results: []
});

// Process items one by one
function processData(items) {
  items.forEach(item => {
    const processed = transform(item);
    data.results.push(processed);
    // UI updates after each item
  });
}

3. Event Logging

js
const logger = state({
  logs: []
});

function log(level, message) {
  logger.logs.push({
    level,
    message,
    timestamp: Date.now()
  });
}

effect(() => {
  console.log('Total logs:', logger.logs.length);
});

log('info', 'App started');
log('warn', 'Low memory');
log('error', 'Connection failed');

4. Multi-select State

js
const selection = state({
  selected: []
});

function toggleSelection(item) {
  const index = selection.selected.indexOf(item);
  
  if (index === -1) {
    // Not selected - add it
    selection.selected.push(item);
  } else {
    // Already selected - remove it
    selection.selected.splice(index, 1);
  }
}

effect(() => {
  document.querySelector('#count').textContent = 
    `${selection.selected.length} selected`;
});

5. Collecting Form Data

js
const survey = state({
  responses: []
});

function submitAnswer(question, answer) {
  survey.responses.push({
    question,
    answer,
    timestamp: Date.now()
  });
}

effect(() => {
  const progress = (survey.responses.length / totalQuestions) * 100;
  document.querySelector('#progress').style.width = `${progress}%`;
});

Common Pitfalls

❌ Pitfall 1: Forgetting Arrays Need Patching After Assignment

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

// Replace with new array
app.items = ['X', 'Y', 'Z'];

// ❌ Won't trigger reactivity!
app.items.push('W');

✅ Solution: Patch after assignment

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

// Replace with new array
app.items = ['X', 'Y', 'Z'];

// Patch the array
ReactiveUtils.patchArray(app, 'items');

// ✅ Now triggers reactivity!
app.items.push('W');

❌ Pitfall 2: Expecting Immediate DOM Updates

js
app.items.push('New item');

// ❌ DOM might not be updated yet
const element = document.querySelector('.item:last-child');
console.log(element); // Might be null

✅ Solution: Use nextTick or effect

js
app.items.push('New item');

// Wait for DOM update
nextTick(() => {
  const element = document.querySelector('.item:last-child');
  console.log(element); // Now exists
});

// Or use effect
effect(() => {
  const items = document.querySelectorAll('.item');
  console.log(`DOM has ${items.length} items`);
});

❌ Pitfall 3: Pushing Reactive Objects

js
const item = state({ name: 'Test' });

// ❌ Pushing the proxy itself
app.items.push(item);

✅ Solution: Push plain objects or use toRaw

js
// Option 1: Push plain object
app.items.push({ name: 'Test' });

// Option 2: Use toRaw if you have a reactive object
const item = state({ name: 'Test' });
app.items.push(toRaw(item));

❌ Pitfall 4: Not Using Return Value

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

app.items.push('C');
// Did it work? How many items now?

✅ Solution: Use return value when needed

js
const newLength = app.items.push('C');
console.log(`Array now has ${newLength} items`);

// Or check length
app.items.push('D');
console.log(`Array now has ${app.items.length} items`);

❌ Pitfall 5: Modifying During Effect

js
effect(() => {
  console.log(app.items.length);
  
  // ❌ Modifying reactive state during effect
  if (app.items.length < 5) {
    app.items.push('Item');
  }
});
// Infinite loop!

✅ Solution: Modify outside effect

js
effect(() => {
  console.log(`Items: ${app.items.length}`);
});

// Modify in separate logic
function ensureMinimumItems() {
  while (app.items.length < 5) {
    app.items.push(`Item ${app.items.length + 1}`);
  }
}

ensureMinimumItems();

Summary

Key Takeaways

  1. Reactive push() adds items to the end of arrays and triggers updates automatically
  2. Works exactly like standard push() - same syntax, same return value
  3. Automatically patches arrays created with state() or reactive()
  4. Need manual patching after replacing arrays with ReactiveUtils.patchArray()
  5. Accepts multiple arguments - push one or many items at once

When to Use push()

  • ✅ Adding items to a list or collection
  • ✅ Building arrays incrementally
  • ✅ Collecting form data or user input
  • ✅ Logging events or messages
  • ✅ Appending API response data

Quick Reference

js
// Basic usage
app.items.push('New item')

// Multiple items
app.items.push('A', 'B', 'C')

// With return value
const length = app.items.push('Item')

// With effects
effect(() => {
  console.log(`Count: ${app.items.length}`)
})
app.items.push('Triggers effect!')

// After array replacement
app.items = ['X', 'Y', 'Z']
ReactiveUtils.patchArray(app, 'items')
app.items.push('W') // Now reactive

Remember: Reactive push() is just normal push() with automatic reactivity - use it naturally and your UI stays in sync! 🎯