Hash Maps in Business Code: Three Patterns from Xiaohongshu, from Failed Image Loads to Related Notes
Hash maps aren't just about O(1) lookups. Using Xiaohongshu as an example, see how they're applied in real business scenarios: fallbacks for failed image loads, related-note recommendations, and feed deduplication.
Why Hash Maps Are Everywhere in Business Code
You've memorized "hash table lookup is O(1)" a hundred times for interviews, yet in real business code, plenty of people still write array.find() everywhere. This article takes a different angle: no principle diagrams, just the business scene. Using Xiaohongshu's image-and-text community as the example, here are the three most frequent processing patterns—and behind every one of them is a hash map.
The conclusion first—three reasons business code chooses hash maps:
- "Finding things by ID" is the most frequent operation: finding a note, finding a user's state, checking whether an image has loaded
- Deduplication is the second most frequent operation: feeds don't push duplicate content, and read notes don't turn red again
- The combination of the two—an "ID → state" mapping—is essentially the body of every client-side cache
Pattern 1: Handling Failed Image Loads — Map<url, failCount>
Failed image loads are routine in a feed: weak networks, CDN hiccups, deleted images. The naive handling is to swap in a placeholder on onerror, but there are two real problems:
Problem 1: Infinite retry flickering. In some environments, the component re-renders after onerror and triggers another load; it fails again and swaps the image again, and what the user sees is a frantic flicker.
Problem 2: Repeating mistakes. The user scrolls away and back, and the same failed URL loads all over again, fails all over again—wasting bandwidth and goodwill.
The solution is a "failure log":
// module-level cache: URL -> failure count (survives components and page lifecycles)
const failCounts = new Map<string, number>();
const FAIL_THRESHOLD = 2; // after 2 failures for the same URL, render the placeholder directly on next visit instead of requesting
export function handleImageError(url: string, img: HTMLImageElement) {
const count = (failCounts.get(url) ?? 0) + 1;
failCounts.set(url, count);
// swap the placeholder first: the card doesn't collapse and content below doesn't shift, regardless of retries
img.src = "/placeholder-failed.svg";
// when count >= FAIL_THRESHOLD, the list rendering logic sees failCounts over the limit,
// and renders the placeholder directly, skipping the original URL—"no more retries" is guaranteed here, not by this function's branching
}Three design points:
- The key is the URL string, because a picture's identity is its URL; the value is the failure count, because the business needs to distinguish "a one-off blip" from "truly dead"
- A threshold, not an all-or-nothing rule: transient CDN jitter accounts for a large share of failures; blind retrying and never retrying are both extremes
- The cache is module-level: it must survive component unmounting—when the user scrolls back, the fact that "this image has already died twice" must still be there
The advanced move is to upgrade failCounts from a Map to an LRU with eviction: a feed can scroll past thousands of images, and an ever-growing Map leaks memory. Browser-side Maps have no capacity limit, so the business has to control it itself (for example, keep only the most recent 500 entries, or only cache URLs that are "confirmed dead," deleting successes right away).
Pattern 2: Per-Note State — Map<noteId, NoteMeta>
A single note involves more client-side state than you'd think: liked or not, saved or not, comment count, read or not, related-notes list. The common structure of all this state is noteId → state object—a natural hash map.
The Read Set: Set<noteId>
The simplest implementation of the "seen" marker isn't adding a field to the note object—it's a standalone Set:
const readNotes = new Set<string>();
// when entering the detail page
readNotes.add(noteId);
// when rendering the list
const isRead = readNotes.has(note.id); // the basis for graying out the titleUse a Set rather than an array, because "has this been seen?" gets asked once per card—O(n) includes on a thousand-item feed means a thousand scans.
Local Cache for Interaction State: Map<noteId, Interaction>
Optimistic updates for likes/saves need a local source of truth:
interface Interaction {
liked: boolean;
likeCount: number;
collected: boolean;
}
const interactions = new Map<string, Interaction>();
function toggleLike(noteId: string) {
const prev = interactions.get(noteId) ?? { liked: false, likeCount: 0, collected: false };
const next = { ...prev, liked: !prev.liked, likeCount: prev.likeCount + (prev.liked ? -1 : 1) };
interactions.set(noteId, next); // update locally first; the UI responds immediately
render(noteId, next);
api.like(noteId, next.liked).catch(() => {
interactions.set(noteId, prev); // roll back on failure—the local source of truth earns its keep
render(noteId, prev);
});
}Without this cache, rolling back a failed optimistic update means hunting for UI state everywhere; with it, a rollback is a single set.
Related Recommendations: A Tag Lookup Index
Related-note recommendations can be heavy (collaborative filtering) or light (same tag). The lightweight approach is an inverted index:
// build: tag -> list of note IDs (build on demand when viewing a detail page, or pre-build for the first screen)
const tagIndex = new Map<string, string[]>();
function buildIndex(notes: Note[]) {
for (const note of notes) {
for (const tag of note.tags) {
const list = tagIndex.get(tag) ?? [];
list.push(note.id);
tagIndex.set(tag, list);
}
}
}
// use: take the current note's highest-weighted tags and look up notes sharing them
function relatedNotes(current: Note, limit = 10): Note[] {
const seen = new Set<string>([current.id]); // deduplicate: never recommend yourself
const result: Note[] = [];
for (const tag of current.tags) {
for (const id of tagIndex.get(tag) ?? []) {
if (seen.has(id)) continue;
seen.add(id);
result.push(id);
if (result.length >= limit) return result;
}
}
return result;
}Notice the nested Set—hash maps in business code never appear alone; they team up with other data structures: Map handles the index, Set handles deduplication, a classic pairing.
Pattern 3: Feed Deduplication — the Exposure Set<noteId>
A feed has an implicit KPI: no duplicate content within the same time window. Pagination, pull-to-refresh, and category switching can all bring the same note back on screen.
The solution is a session-level exposure log:
const exposed = new Set<string>();
function mergeFeed(fresh: Note[]): Note[] {
return fresh.filter((note) => {
if (exposed.has(note.id)) return false; // already exposed, filter it out
exposed.add(note.id); // note: register it as you filter
return true;
});
}The key detail: register when content "enters the list," not when it is "seen by the user." If you register via an exposure observer (IntersectionObserver), notes that scroll past quickly can be missed; registering on entry to the list carries the meaning "I had the opportunity to show this," which is simple and misses nothing.
The boundaries of the exposure log need careful thought: should it clear on page refresh? On category switch? Usually "current session + current category" is the right granularity—clearing everything makes the home page repeat after a refresh, while never clearing makes users complain "why is it always these few?"
Why Not Object Literals or Arrays?
Three common questions, answered once:
Q: Isn't {} also a key-value store? Why use Map?
{}keys can only be strings/Symbols, so numericnoteIds require implicit conversion;Mapkeys can be of any type{}inherits keys liketoStringfrom the prototype chain, so user data with a key like"constructor"triggers prototype pollution ({}["constructor"]doesn't throw but returns nonsense)Maphassize; with{}you needObject.keys().lengthMapperformance is steadier under frequent additions and deletions (deleting keys on{}can trigger hidden-class deoptimization)
Q: How do you choose between Set and Map?
If you only care about "is it there?", use a Set (read state, exposure); if you care about "it's there, and it carries data," use a Map (interaction state, failure counts). There's no gray area.
Q: Isn't array find intuitive enough?
Once the data reaches three digits, it's a disaster. Business lists easily run to hundreds or thousands of entries, and calling find once per card on every render is the classic "comfortable to write, janky to run." Once lookup frequency × data size exceeds roughly 50, it's time to switch to a hash structure.
Engineering Details: Don't Let the Cache Become a Memory Leak
The business risk of hash maps is never "not knowing how to use them"—it's "forgetting about size":
- Only cache what can be evicted: set caps for failed URLs and interaction state (the LRU idea: when capacity is exceeded, evict the least recently used first)
- Session-level state dies with the session: clear the exposure Set on page unload/category switch per your policy—don't expect it to disappear on its own
- Build inverted indexes on demand: building indexes for all tags upfront is worse than querying by the current tag when entering a detail page—indexes have build costs too
Summary
Looking back at the three patterns, their commonality is striking:
| Business problem | Structure | Key | Value |
|---|---|---|---|
| Image load fallback | Map | Image URL | Failure count |
| Note interaction state | Map | Note ID | Interaction state object |
| Read/exposure deduplication | Set | Note ID | — |
| Related-recommendation index | Map + Set | Tag | List of note IDs |
The role of hash maps in business code is, in essence, giving "identity" an O(1) answer: has this note been liked, is this image dead, has this content been pushed. Next time you reach for find while writing business code, pause for a second—maybe the thing you're looking for should have had a table all along.