javascript
39 lines · 7 steps
Building a stateful table sorter in JavaScript
A closure remembers the last sorted column so repeat clicks flip direction, using a stable comparator that handles nulls and mixed types.
Explained by
highlit
1function makeTableSorter(rows) {
2 const state = { key: null, dir: 1 };
3
4 const compareBy = (key) => (a, b) => {
5 const av = a[key];
6 const bv = b[key];
7 if (av == null && bv == null) return 0;
8 if (av == null) return 1;
9 if (bv == null) return -1;
10 if (typeof av === 'number' && typeof bv === 'number') {
11 return av - bv;
12 }
13 return String(av).localeCompare(String(bv), undefined, {
14 numeric: true,
15 sensitivity: 'base',
16 });
17 };
18
19 const stableSort = (arr, cmp) =>
20 arr
21 .map((item, index) => ({ item, index }))
22 .sort((x, y) => cmp(x.item, y.item) || x.index - y.index)
23 .map(({ item }) => item);
24
25 return function sortByColumn(key) {
26 if (state.key === key) {
27 state.dir *= -1;
28 } else {
29 state.key = key;
30 state.dir = 1;
31 }
32
33 const base = compareBy(key);
34 const cmp = (a, b) => base(a, b) * state.dir;
35
36 rows = stableSort(rows, cmp);
37 return { rows, sortedBy: key, direction: state.dir === 1 ? 'asc' : 'desc' };
38 };
39}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Closing over a state object lets a returned function remember context across calls without globals.
- 2Decorate-sort-undecorate turns any comparator into a stable sort by using original index as a tiebreaker.
- 3A robust comparator must decide null ordering and type handling explicitly rather than relying on default coercion.
Related explainers
javascript
const express = require('express'); const router = express.Router(); const { pool } = require('../db'); const redis = require('../redis');
Building a health check endpoint in Express
health-check
timeouts
promise-race
Intermediate
9 steps
javascript
'use client'; import { useEffect } from 'react'; import * as Sentry from '@sentry/nextjs';
How a Next.js error boundary recovers
error-boundary
error-handling
observability
Intermediate
8 steps
typescript
type ResizeCallback = (size: { width: number; height: number }) => void; export function observeResize(callback: ResizeCallback, delay = 150) { let timeoutId: ReturnType<typeof setTimeout> | undefined;
Debouncing window resize in TypeScript
debounce
closures
event-listeners
Intermediate
6 steps
javascript
const MAX_BATCH_SIZE = 20; const FLUSH_INTERVAL = 5000; const ENDPOINT = '/api/analytics';
Batching analytics events in the browser
batching
buffering
sendbeacon
Intermediate
10 steps
rust
use axum::{ extract::State, routing::{get, post, MethodRouter}, Json, Router,
Self-documenting routes in Axum
builder-pattern
closures
shared-state
Intermediate
8 steps
javascript
export function diffIds(current, desired) { const currentSet = new Set(current); const desiredSet = new Set(desired);
Diffing sets to sync group membership
set-operations
diffing
transactions
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-stateful-table-sorter-in-javascript-explained-javascript-83ce/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.