Dedupe Key Picker

Previews how many rows a dedupe would merge as you pick key columns, so it doesn't merge nothing or merge too much.

StopsDedupe merged the wrong rows

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/dedupe-key-picker.json

The files land in your project. They're yours to edit.

Usage

dedupe-key-picker-usage.tsx
import { DedupeKeyPicker } from "@/components/ui/dedupe-key-picker"

<DedupeKeyPicker
  rows={contacts}
  columns={["email", "phone", "city", "signed_up"]}
  defaultValue={["email", "signed_up"]}
  onApply={() => new Promise((resolve) => setTimeout(resolve, 600))}
/>

What it catches

  • Near-unique columns (timestamps, ids) that prevent any merge, with "leave it out"
  • Keys so broad that one value covers a large share of rows (coarseShare)
  • Blank keys, kept separate instead of merged together
  • Values a merge would discard, shown in sample collisions

Props

DedupeKeyPicker props
PropTypeDefault
rows* (required)

The rows to deduplicate. A sample of a few thousand is plenty.

DedupeRow[]—
columns

Columns offered as key candidates. Defaults to the keys of the first row.

string[]—
value

Controlled key columns.

string[]—
defaultValue

Initial key columns when uncontrolled.

string[][]
onValueChange

Called with the chosen key columns.

(keys: string[]) => void—
normalize

Controlled "ignore case and spaces".

boolean—
defaultNormalize

Initial "ignore case and spaces" when uncontrolled.

booleantrue
onNormalizeChange

Called when normalisation is turned on or off.

(normalize: boolean) => void—
onApply

Runs the merge. May return a Promise; the button shows a pending state.

(plan: { keys: string[]; normalize: boolean }) => void | Promise<unknown>—
samples

Groups shown as sample collisions.

number2
coarseShare

Share of rows one key may absorb before it is called too broad.

number0.25
locale

BCP 47 locale for numbers.

string—
messages

Every visible string, with `{placeholders}`.

Partial<DedupeKeyPickerMessages>—

* required. Also accepts the native props of <fieldset> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Column toggles are buttons with aria-pressed and their uniqueness share in text
  • The preview is aria-live="polite"
  • Apply sets aria-busy while running; errors use role="alert"

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Run the merge in a transaction on the server with a backup or undo path; the preview is computed from the rows you pass.

  • Works on the rows you pass; on a sample, counts are estimates for the full table
  • Normalisation (case, spaces) is optional and simple

What it stops

Dedupe merged the wrong rows

Also known as

dedupe · remove duplicates · merge records · crm contacts · data cleaning · unique key · pandas · csv import

Field type

Dependencies

Pairs well with