Skip to content

Sponsor

Sponsor

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

Quick Start (30 seconds)

Need to sort a reactive array? Just use sort():

js
const app = state({
  numbers: [5, 2, 8, 1, 9]
});

// Set up a watcher
effect(() => {
  console.log('Numbers:', app.numbers.join(', '));
});
// Logs: "Numbers: 5, 2, 8, 1, 9"

// Sort in place
app.numbers.sort((a, b) => a - b);
// Logs: "Numbers: 1, 2, 5, 8, 9" (reactivity triggered!)

// Sort strings
const app2 = state({
  names: ['Charlie', 'Alice', 'Bob']
});

app2.names.sort();
// Logs: "Names: Alice, Bob, Charlie"

That's it! sort() sorts reactive arrays in place and automatically triggers updates!

What is Reactive sort()?

The reactive sort() method is an enhanced version of the standard array sort() method that automatically triggers reactive updates when the array is sorted in place.

This method:

  • Sorts array elements in place (modifies the original array)
  • Returns the sorted array
  • Automatically triggers reactive effects, watchers, and bindings
  • Works exactly like standard Array.prototype.sort()
  • Accepts optional compare function for custom sorting

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

Syntax

js
// Default sort (converts to strings)
array.sort()

// Custom sort with compare function
array.sort((a, b) => a - b)

// Full examples
const app = state({
  numbers: [3, 1, 4, 1, 5]
});

// Sort numbers ascending
app.numbers.sort((a, b) => a - b);  // [1, 1, 3, 4, 5]

// Sort numbers descending
app.numbers.sort((a, b) => b - a);  // [5, 4, 3, 1, 1]

// Sort strings
const app2 = state({
  names: ['Charlie', 'Alice', 'Bob']
});

app2.names.sort();  // ['Alice', 'Bob', 'Charlie']

Parameters:

  • compareFn (optional) - Function that defines sort order
    • compareFn(a, b) returns:
      • Negative value: a comes before b
      • Zero: keep original order
      • Positive value: b comes before a

Returns:

  • The sorted array (same reference, sorted in place)

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 = [3, 1, 2];

items.sort(); // 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 = [3, 1, 2]                             │
│      ↓                                          │
│  items.sort()  ← Mutation happens              │
│      ↓                                          │
│  items = [1, 2, 3]                             │
│                                                 │
│  ❌ Effects don't run                          │
│  ❌ Watchers don't trigger                     │
│  ❌ UI doesn't update                          │
│                                                 │
└─────────────────────────────────────────────────┘

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

The Solution

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

  1. The normal sort() behavior happens (array sorted in place)
  2. The reactive system is notified of the change
  3. All effects, watchers, and bindings automatically update

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

Mental Model

Think of reactive sort() like organizing books on a shelf with automatic catalog updates:

Standard Array (Manual Process):

You rearrange books by title
→ Books are sorted
→ You manually update the catalog
→ You manually update the index
→ You manually notify librarian

Reactive Array (Automatic Process):

You rearrange books by title
→ Books are sorted
→ Catalog updates automatically
→ Index regenerates automatically
→ Librarian notified automatically
→ Search results refresh automatically

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

How Does It Work?

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

js
// Simplified implementation
function patchSort(array, state, key) {
  const originalSort = Array.prototype.sort;
  
  array.sort = function(compareFn) {
    // 1. Call the original sort method
    const result = originalSort.apply(this, compareFn ? [compareFn] : []);
    
    // 2. Notify the reactive system
    const updatedArray = [...this];
    state[key] = updatedArray; // Triggers reactivity!
    
    // 3. Return the sorted array (like normal sort)
    return result;
  };
}

The process:

  1. You call sort() on a reactive array
  2. Original behavior happens - Array sorted in place
  3. Reactive notification - System detects the change
  4. Effects re-run - Anything watching the array updates
  5. Returns sorted array - Just like standard sort()

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

Basic Usage

Sorting Numbers

js
const app = state({
  scores: [95, 72, 88, 100, 65]
});

// Ascending order
app.scores.sort((a, b) => a - b);
console.log(app.scores);  // [65, 72, 88, 95, 100]

// Descending order
app.scores.sort((a, b) => b - a);
console.log(app.scores);  // [100, 95, 88, 72, 65]

Sorting Strings

js
const app = state({
  names: ['Charlie', 'Alice', 'Bob', 'David']
});

// Alphabetical order
app.names.sort();
console.log(app.names);  // ['Alice', 'Bob', 'Charlie', 'David']

// Reverse alphabetical
app.names.sort((a, b) => b.localeCompare(a));
console.log(app.names);  // ['David', 'Charlie', 'Bob', 'Alice']

Sorting Objects

js
const app = state({
  users: [
    { name: 'Bob', age: 30 },
    { name: 'Alice', age: 25 },
    { name: 'Charlie', age: 35 }
  ]
});

// Sort by age
app.users.sort((a, b) => a.age - b.age);
// Users sorted: Alice(25), Bob(30), Charlie(35)

// Sort by name
app.users.sort((a, b) => a.name.localeCompare(b.name));
// Users sorted: Alice, Bob, Charlie

With Effects

js
const app = state({
  items: [3, 1, 4, 1, 5]
});

effect(() => {
  console.log('Items in order:', app.items.join(' → '));
});
// Logs: "Items in order: 3 → 1 → 4 → 1 → 5"

app.items.sort((a, b) => a - b);
// Logs: "Items in order: 1 → 1 → 3 → 4 → 5"

Advanced Usage

Sortable Table

js
const table = state({
  data: [
    { id: 1, name: 'Alice', age: 30, salary: 70000 },
    { id: 2, name: 'Bob', age: 25, salary: 60000 },
    { id: 3, name: 'Charlie', age: 35, salary: 80000 }
  ],
  sortBy: 'name',
  sortDir: 'asc'
});

function sortTable(column) {
  // Toggle direction if same column
  if (table.sortBy === column) {
    table.sortDir = table.sortDir === 'asc' ? 'desc' : 'asc';
  } else {
    table.sortBy = column;
    table.sortDir = 'asc';
  }
  
  // Sort the data
  table.data.sort((a, b) => {
    let aVal = a[column];
    let bVal = b[column];
    
    if (typeof aVal === 'string') {
      return table.sortDir === 'asc' 
        ? aVal.localeCompare(bVal)
        : bVal.localeCompare(aVal);
    }
    
    return table.sortDir === 'asc' 
      ? aVal - bVal
      : bVal - aVal;
  });
}

effect(() => {
  console.log(`Sorted by ${table.sortBy} (${table.sortDir})`);
  table.data.forEach(row => console.log(row.name));
});

sortTable('age');   // Sort by age ascending
sortTable('age');   // Sort by age descending
sortTable('name');  // Sort by name ascending

Multi-Level Sorting

js
const app = state({
  products: [
    { category: 'Electronics', name: 'Phone', price: 800 },
    { category: 'Electronics', name: 'Laptop', price: 1200 },
    { category: 'Books', name: 'Novel', price: 15 },
    { category: 'Books', name: 'Textbook', price: 90 }
  ]
});

// Sort by category, then by price
app.products.sort((a, b) => {
  // First, compare category
  const catCompare = a.category.localeCompare(b.category);
  if (catCompare !== 0) return catCompare;
  
  // If same category, compare price
  return a.price - b.price;
});

effect(() => {
  app.products.forEach(p => {
    console.log(`${p.category}: ${p.name} - $${p.price}`);
  });
});

Case-Insensitive Sort

js
const app = state({
  tags: ['JavaScript', 'react', 'Vue', 'ANGULAR', 'Node']
});

// Case-insensitive sort
app.tags.sort((a, b) => {
  return a.toLowerCase().localeCompare(b.toLowerCase());
});

effect(() => {
  console.log('Tags:', app.tags.join(', '));
});
// Logs: "Tags: ANGULAR, JavaScript, Node, react, Vue"

Sorting with Null/Undefined

js
const app = state({
  items: [3, null, 1, undefined, 5, 2]
});

// Handle null/undefined values
app.items.sort((a, b) => {
  // Push null/undefined to end
  if (a == null) return 1;
  if (b == null) return -1;
  
  return a - b;
});

console.log(app.items);
// [1, 2, 3, 5, null, undefined]

Date Sorting

js
const app = state({
  events: [
    { name: 'Meeting', date: new Date('2025-03-15') },
    { name: 'Deadline', date: new Date('2025-02-01') },
    { name: 'Launch', date: new Date('2025-04-10') }
  ]
});

// Sort by date (oldest first)
app.events.sort((a, b) => a.date - b.date);

effect(() => {
  app.events.forEach(e => {
    console.log(`${e.name}: ${e.date.toLocaleDateString()}`);
  });
});

Common Patterns

1. Toggle Sort Direction

js
const app = state({
  items: [3, 1, 4, 1, 5],
  ascending: true
});

function toggleSort() {
  app.ascending = !app.ascending;
  app.items.sort((a, b) => {
    return app.ascending ? a - b : b - a;
  });
}

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

2. Sort by Property

js
const app = state({
  users: [
    { name: 'Bob', score: 85 },
    { name: 'Alice', score: 92 },
    { name: 'Charlie', score: 78 }
  ]
});

function sortBy(property) {
  app.users.sort((a, b) => {
    if (typeof a[property] === 'string') {
      return a[property].localeCompare(b[property]);
    }
    return a[property] - b[property];
  });
}

sortBy('score');  // Sort by score
sortBy('name');   // Sort by name

3. Maintain Sorted State

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

function addItemSorted(item) {
  app.items.push(item);
  app.items.sort((a, b) => a - b);
}

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

addItemSorted(5);
addItemSorted(2);
addItemSorted(8);
// Always maintains sorted order

4. Custom Comparator

js
const app = state({
  files: [
    { name: 'document.pdf', size: 1024000 },
    { name: 'image.jpg', size: 512000 },
    { name: 'video.mp4', size: 10240000 }
  ]
});

// Sort by size with custom logic
app.files.sort((a, b) => {
  // Sort by size, but put videos first
  const aIsVideo = a.name.endsWith('.mp4');
  const bIsVideo = b.name.endsWith('.mp4');
  
  if (aIsVideo && !bIsVideo) return -1;
  if (!aIsVideo && bIsVideo) return 1;
  
  return a.size - b.size;
});

5. Locale-Aware Sorting

js
const app = state({
  names: ['Ömer', 'Alice', 'Zoë', 'Bob', 'Åsa']
});

// Locale-aware sort
app.names.sort((a, b) => {
  return a.localeCompare(b, 'en', { sensitivity: 'base' });
});

console.log(app.names);
// Correctly handles special characters

Common Pitfalls

❌ Pitfall 1: Forgetting Compare Function for Numbers

js
const app = state({
  numbers: [10, 5, 40, 25, 1000, 1]
});

// ❌ Sorts as strings: "1", "10", "1000", "25", "40", "5"
app.numbers.sort();
console.log(app.numbers);  // [1, 10, 1000, 25, 40, 5]

✅ Solution: Always use compare function for numbers

js
const app = state({
  numbers: [10, 5, 40, 25, 1000, 1]
});

// Correct numeric sort
app.numbers.sort((a, b) => a - b);
console.log(app.numbers);  // [1, 5, 10, 25, 40, 1000]

❌ Pitfall 2: Expecting Immutable Sort

js
const app = state({
  original: [3, 1, 2]
});

// ❌ sort() modifies in place!
const sorted = app.original.sort((a, b) => a - b);
console.log(app.original);  // [1, 2, 3] (modified!)

✅ Solution: Copy first if you need original

js
const app = state({
  original: [3, 1, 2]
});

// Create copy, then sort
const sorted = [...app.original].sort((a, b) => a - b);
console.log(app.original);  // [3, 1, 2] (unchanged)
console.log(sorted);        // [1, 2, 3]

❌ Pitfall 3: Comparing Objects Incorrectly

js
const app = state({
  items: [{ value: 2 }, { value: 1 }, { value: 3 }]
});

// ❌ Doesn't work - comparing objects
app.items.sort((a, b) => a - b);

✅ Solution: Compare properties

js
const app = state({
  items: [{ value: 2 }, { value: 1 }, { value: 3 }]
});

// Compare the value property
app.items.sort((a, b) => a.value - b.value);

❌ Pitfall 4: Case-Sensitive String Sort

js
const app = state({
  names: ['alice', 'Bob', 'charlie', 'Alice']
});

// ❌ Case-sensitive: capitals come before lowercase
app.names.sort();
console.log(app.names);  // ['Alice', 'Bob', 'alice', 'charlie']

✅ Solution: Use case-insensitive compare

js
const app = state({
  names: ['alice', 'Bob', 'charlie', 'Alice']
});

// Case-insensitive sort
app.names.sort((a, b) => {
  return a.toLowerCase().localeCompare(b.toLowerCase());
});
console.log(app.names);  // ['alice', 'Alice', 'Bob', 'charlie']

❌ Pitfall 5: Arrays Need Patching After Assignment

js
const app = state({
  items: [3, 1, 2]
});

// Replace with new array
app.items = [5, 2, 8];

// ❌ Won't trigger reactivity!
app.items.sort((a, b) => a - b);

✅ Solution: Patch after assignment

js
const app = state({
  items: [3, 1, 2]
});

// Replace with new array
app.items = [5, 2, 8];

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

// ✅ Now triggers reactivity!
app.items.sort((a, b) => a - b);

Summary

Key Takeaways

  1. Reactive sort() sorts arrays in place and triggers updates automatically
  2. Returns the sorted array (same reference)
  3. Always use compare function for numbers to avoid string sorting
  4. Sorts in place - original array is modified
  5. Accepts optional compareFn for custom sorting logic

When to Use sort()

  • ✅ Sorting lists and tables
  • ✅ Organizing data by priority
  • ✅ Alphabetizing names
  • ✅ Ordering by date or number
  • ✅ Multi-level sorting

Quick Reference

js
// Sort numbers ascending
app.numbers.sort((a, b) => a - b)

// Sort numbers descending
app.numbers.sort((a, b) => b - a)

// Sort strings
app.names.sort()

// Sort strings (case-insensitive)
app.names.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))

// Sort objects by property
app.items.sort((a, b) => a.price - b.price)

// Reverse sort
app.items.sort((a, b) => b.value - a.value)

// After array replacement
app.items = [3, 1, 2]
ReactiveUtils.patchArray(app, 'items')
app.items.sort((a, b) => a - b) // Now reactive

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