Understanding array.sort() in Reactive Arrays - A Beginner's Guide
Quick Start (30 seconds)
Need to sort a reactive array? Just use sort():
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
// 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 ordercompareFn(a, b)returns:- Negative value:
acomes beforeb - Zero: keep original order
- Positive value:
bcomes beforea
- Negative value:
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:
const items = [3, 1, 2];
items.sort(); // Array changed, but no one knows!
// UI doesn't update, effects don't runWhat'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:
- The normal
sort()behavior happens (array sorted in place) - The reactive system is notified of the change
- 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 librarianReactive Array (Automatic Process):
You rearrange books by title
→ Books are sorted
→ Catalog updates automatically
→ Index regenerates automatically
→ Librarian notified automatically
→ Search results refresh automaticallyThe 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:
// 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:
- You call
sort()on a reactive array - Original behavior happens - Array sorted in place
- Reactive notification - System detects the change
- Effects re-run - Anything watching the array updates
- 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
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
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
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, CharlieWith Effects
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
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 ascendingMulti-Level Sorting
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
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
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
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
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
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 name3. Maintain Sorted State
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 order4. Custom Comparator
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
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 charactersCommon Pitfalls
❌ Pitfall 1: Forgetting Compare Function for Numbers
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
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
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
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
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
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
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
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
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
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
- Reactive
sort()sorts arrays in place and triggers updates automatically - Returns the sorted array (same reference)
- Always use compare function for numbers to avoid string sorting
- Sorts in place - original array is modified
- 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
// 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 reactiveRemember: Reactive sort() is just normal sort() with automatic reactivity - use it naturally and your UI stays in sync! 🎯