Set vs filter vs lodash — Why Your Dedupe Still Leaves Duplicates

(Updated: July 16, 2026 ) JavaScript dedupe webdev

Classic r/javascript thread: “Just use [...new Set(arr)]” — then OP replies that duplicates are still there because the array contains objects, lines differ by trailing spaces, or CSV rows look identical but have different hidden characters. The one-liner isn’t wrong; it’s answering the wrong question.

  • Set dedupes objects by reference[{id:1},{id:1}] has length 2 after Set. You need a key.
  • Trim matters"foo" and " foo " are different strings. Excel exports love trailing spaces.
  • uniqBy without the right key — Keeps the first {id:1, name:"old"} and drops {id:1, name:"new"} silently.
  • “Dedupe then sort” vs “sort then dedupe” — Order of operations changes which duplicate survives when you use Map.

String arrays and line lists

The happy path — unique strings, no trim issues:

const tags = ["react", "react", "vue"];
const unique = [...new Set(tags)];
// ["react", "vue"]

Real-world export from CRM — trim + lowercase for email cleanup:

const raw = `[email protected]
[email protected]
 [email protected] `;

const lines = raw.split("\n").map((l) => l.trim().toLowerCase());
const unique = [...new Set(lines)].filter(Boolean);

For 10k+ lines pasted from a spreadsheet, a browser dedupe tool beats writing a one-off script on a locked-down laptop — trim, sort, unique, copy.

Array of objects — pick a key explicitly

const rows = [
  { id: 1, label: "first" },
  { id: 1, label: "duplicate id" },
  { id: 2, label: "ok" },
];

// Keep last occurrence per id
const byId = [...new Map(rows.map((r) => [r.id, r])).values()];
ApproachKeepsBest for
Set(arr)N/A for objectsPrimitives only
Map by keyLast wins (above)Configurable
filter + findIndexFirst winsStable “first seen”
lodash uniqByFirst wins by defaultQuick scripts

When duplicates aren’t duplicates

  • Unicode normalization"café" vs "café" (composed vs decomposed). Normalize with normalize("NFC") before Set.
  • Windows \r\n — Split on \r?\n before comparing lines.
  • Empty lines — Decide if you want to keep or drop blanks before dedupe.
  • Case sensitivity — Usernames vs emails have different rules.

Performance reality check

SizeSet + trimSort + unique in tool
< 1k linesEither fineEither fine
10k–100kFine in modern JSBrowser tool OK
1M+Stream or DBDon’t paste in browser

For one-off marketing list cleanup, correctness beats Big-O notation.

Objects need identity, not Set

Set dedupes by reference for objects — [{id:1},{id:1}] stays length 2. Use Map keyed by id, or serialize stable JSON strings if order doesn’t matter:

const uniqueById = [...new Map(items.map((x) => [x.id, x])).values()];

Same trap hits filter((v,i,a)=>a.indexOf(v)===i) on object arrays — indexOf compares references, not deep equality.

Step-by-step: Mailchimp export cleanup

  1. Export CSV from source system.
  2. Paste email column into dedupe tool — enable trim, optional sort.
  3. Scan count: input lines vs output lines — delta should match expected dupes.
  4. Spot-check 5 removed entries manually (typos vs true dupes).
  5. Import cleaned list; keep raw export in cold storage for audit.

Tool: dedupe locally

The dedupe tool processes lists in your browser — no upload, no account. Paste lines, toggle trim/sort, copy unique output.

Try the dedupe tool

TL;DR

Set works for clean string arrays. Real data needs trim, normalization, and key-based logic for objects. When in doubt, paste into a local dedupe tool, verify counts, then ship.