---
title: Detect Duplicate Form Records
product: Form Library
description: Detect duplicate records in SurveyJS forms by comparing new entries against existing data. View this JavaScript demo to learn more.
framework: React
source: https://surveyjs.io/form-library/examples/detect-duplicate-form-records/reactjs
index: https://surveyjs.io/form-library/examples/overview.md
---

# Detect Duplicate Form Records (React)

Duplicate detection checks whether a newly entered record matches—or is likely to match—an existing record, helping prevent duplicate patients, customers, or registrations. Unlike [mis-keyed variation detection](/form-library/examples/detect-miskeyed-form-entries/), which flags possible spelling mistakes, duplicate detection determines whether the person or organization already exists and whether a new record should be created.

To implement duplicate detection in SurveyJS, handle the [`onServerValidateQuestions`](/form-library/documentation/surveymodel#onServerValidateQuestions) event, compare the submitted data against your registry using exact and approximate matching, populate the `errors` object with field-level messages, and call `complete()`.

## How This Demo Works

1. Click **Fill Sample Data** to load a near-match example (`Jon Smith` with DOB `1987-04-12`), or enter values manually.
2. When you click **Register Participant**, SurveyJS raises the `onServerValidateQuestions` event.
3. The event handler compares the submitted data with an in-demo participant list (with a short delay to simulate an API call).
4. Exact email or participant ID matches produce duplicate errors. Strong matches based on name, date of birth, and phone number produce possible-duplicate warnings. Messages do not reveal details of existing records.

| Try this | Expected result after submission |
| --- | --- |
| Email `john.smith@email.com` | Duplicate detected (email) |
| Participant ID `EXT-9001` | Duplicate detected (participant ID) |
| Name `Jon Smith` + DOB `1987-04-12` | Possible duplicate |
| Name `Sofia Martinezz` + DOB `1985-01-22` | Possible duplicate |

## Connect to an API Endpoint

To check against your CRM, EHR, or study database, call your duplicate-detection endpoint from `onServerValidateQuestions`. Populate the `errors` object with the returned messages, then call `complete()`:

```js
function validateDuplicateRecordsAsync(_, { data, errors, complete }) {
  fetch("/api/validation/duplicates", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      fullName: data.fullName,
      dateOfBirth: data.dateOfBirth,
      email: data.email,
      phone: data.phone,
      participantId: data.participantId
    })
  })
    .then((response) => response.json())
    .then((result) => {
      // Example: { errors: { email: "Duplicate detected..." } }
      Object.keys(result.errors || {}).forEach((name) => {
        errors[name] = result.errors[name];
      });
      complete();
    })
    .catch(() => {
      errors["fullName"] = "Duplicate check service is temporarily unavailable. Try again.";
      complete();
    });
}

survey.onServerValidateQuestions.add(validateDuplicateRecordsAsync);
```

## Related Examples

- [Server-Side Form Validation Using Expressions](/form-library/examples/javascript-async-form-validation/)
- [Server-Side Form Validation Using an Event](/form-library/examples/javascript-server-side-form-validation/)
- [Double-Entry Verification](/form-library/examples/double-entry-verification/)

## Files

### `public/index.html`

```html
<div id="surveyElement" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; min-height: 100%; height:100%"></div>
```

### `src/SurveyComponent.jsx`

```js
import React from "react";
import { Model } from "survey-core";
import { Survey } from "survey-react-ui";
import "survey-core/survey-core.min.css";
import "./index.css";
import { json } from "./json";

// Duplicate detection via onServerValidateQuestions.
// Demo uses an in-memory registry; see the Documentation tab to connect your own API.

const EXISTING_PARTICIPANTS = [
  {
    fullName: "John Smith",
    dateOfBirth: "1987-04-12",
    email: "john.smith@email.com",
    phone: "+1 555 0100",
    participantId: "EXT-9001"
  },
  {
    fullName: "Katherine Brown",
    dateOfBirth: "1991-09-03",
    email: "kbrown@email.com",
    phone: "+1 555 0144",
    participantId: "EXT-9002"
  },
  {
    fullName: "Sofia Martinez",
    dateOfBirth: "1985-01-22",
    email: "s.martinez@email.com",
    phone: "+1 555 0199",
    participantId: "EXT-9003"
  },
  {
    fullName: "ACME Corporation Contact",
    dateOfBirth: "1980-01-01",
    email: "ops@acme.example",
    phone: "+1 555 0200",
    participantId: "EXT-9010"
  }
];

function normalizeText(value) {
  return String(value || "")
    .toLowerCase()
    .replace(/[^a-z0-9\s+]/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function normalizeEmail(value) {
  return String(value || "").trim().toLowerCase();
}

function normalizePhone(value) {
  return String(value || "").replace(/\D/g, "");
}

function levenshteinDistance(a, b) {
  const rows = a.length + 1;
  const cols = b.length + 1;
  const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));

  for (let i = 0; i < rows; i++) matrix[i][0] = i;
  for (let j = 0; j < cols; j++) matrix[0][j] = j;

  for (let i = 1; i < rows; i++) {
    for (let j = 1; j < cols; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      matrix[i][j] = Math.min(
        matrix[i - 1][j] + 1,
        matrix[i][j - 1] + 1,
        matrix[i - 1][j - 1] + cost
      );
    }
  }

  return matrix[a.length][b.length];
}

function similarityRatio(left, right) {
  const a = normalizeText(left);
  const b = normalizeText(right);
  if (!a || !b) return 0;
  if (a === b) return 1;
  const distance = levenshteinDistance(a, b);
  return 1 - distance / Math.max(a.length, b.length);
}

function scoreDuplicate(candidate, existing) {
  const emailExact =
    normalizeEmail(candidate.email) &&
    normalizeEmail(candidate.email) === normalizeEmail(existing.email);
  const idExact =
    normalizeText(candidate.participantId) &&
    normalizeText(candidate.participantId) === normalizeText(existing.participantId);
  const phoneExact =
    normalizePhone(candidate.phone).length >= 7 &&
    normalizePhone(candidate.phone) === normalizePhone(existing.phone);
  const dobExact =
    candidate.dateOfBirth &&
    candidate.dateOfBirth === existing.dateOfBirth;
  const nameScore = similarityRatio(candidate.fullName, existing.fullName);

  if (emailExact || idExact) {
    return {
      confidence: 100,
      kind: "exact",
      reason: emailExact
        ? "email already registered"
        : "participant ID already registered",
      record: existing,
      focusField: emailExact ? "email" : "participantId"
    };
  }

  if (nameScore >= 0.72 && dobExact) {
    let confidence = Math.round(70 + nameScore * 25);
    if (phoneExact) {
      confidence = Math.min(99, confidence + 8);
    }
    return {
      confidence,
      kind: "near",
      reason: phoneExact
        ? "name, date of birth, and phone"
        : "name and date of birth",
      record: existing,
      focusField: "fullName"
    };
  }

  if (nameScore >= 0.9 && phoneExact) {
    return {
      confidence: Math.round(80 + nameScore * 15),
      kind: "near",
      reason: "name and phone",
      record: existing,
      focusField: "fullName"
    };
  }

  return null;
}

function findDuplicate(candidate) {
  let best = null;

  EXISTING_PARTICIPANTS.forEach((existing) => {
    const hit = scoreDuplicate(candidate, existing);
    if (!hit) return;
    if (!best || hit.confidence > best.confidence) {
      best = hit;
    }
  });

  return best;
}

// In-demo async check (replace with fetch to your duplicate-detection API in production).
function mockValidationApi(candidate) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const errors = {};

      if (!candidate.fullName && !candidate.email && !candidate.participantId) {
        resolve({ errors });
        return;
      }

      const match = findDuplicate(candidate);
      if (!match) {
        resolve({ errors });
        return;
      }

      const { confidence, kind, reason, focusField } = match;

      if (kind === "exact") {
        errors[focusField] =
          `Duplicate detected (${reason}). ` +
          "Review the entered data before creating a new record.";
      } else if (confidence >= 80) {
        errors[focusField] =
          `Possible duplicate (${confidence}% match — ${reason}). ` +
          "Review the entered data and verify that this is a new participant.";
      }

      resolve({ errors });
    }, 450);
  });
}

function validateDuplicateRecordsAsync(_, { data, errors, complete }) {
  mockValidationApi({
    fullName: data["fullName"],
    dateOfBirth: data["dateOfBirth"],
    email: data["email"],
    phone: data["phone"],
    participantId: data["participantId"]
  })
    .then((result) => {
      Object.keys(result.errors || {}).forEach((name) => {
        errors[name] = result.errors[name];
      });
      complete();
    })
    .catch(() => {
      errors["fullName"] = "Duplicate check service is temporarily unavailable. Try again.";
      complete();
    });
}

function SurveyComponent() {
    const survey = new Model(json);
    survey.onComplete.add((sender, options) => {
        console.log(JSON.stringify(sender.data, null, 3));
    });
    survey.onServerValidateQuestions.add(validateDuplicateRecordsAsync);
    
    survey.addNavigationItem({
      id: "sv-nav-fill-sample",
      title: "Fill Sample Data",
      action: () => {
        // Near-match demo: similar name + same DOB as an existing registry record
        survey.setValue("fullName", "Jon Smith");
        survey.setValue("dateOfBirth", "1987-04-12");
        survey.setValue("email", "jon.smith.new@email.com");
        survey.setValue("phone", "+1 555 0100");
        survey.setValue("participantId", "");
      },
      css: "nav-button",
      innerCss: "sd-btn"
    });
    
    return (<Survey model={survey} />);
}

export default SurveyComponent;
```

### `src/index.css`

```css
.nav-button {
  margin-left: auto;
  margin-right: 0;
}
```

### `src/index.js`

```js
import React from "react";
import { createRoot } from "react-dom/client";
import SurveyComponent from "./SurveyComponent";

const root = createRoot(document.getElementById("surveyElement"));
root.render(<SurveyComponent />);
```

### `src/json.js`

```js
export const json = {
  "title": "Participant Registration — Duplicate Check",
  "description": "Register a participant against a demo study registry. On submission, async validation checks for duplicate records using exact email or participant ID matches and possible matches based on name and date of birth. Try \"Jon Smith\" with DOB \"1987-04-12\", or enter an existing email address.",
  "completeText": "Register Participant",
  "pages": [
    {
      "name": "registration",
      "elements": [
        {
          "type": "text",
          "name": "fullName",
          "title": "Full name",
          "isRequired": true,
          "placeholder": "e.g. Jon Smith"
        },
        {
          "type": "text",
          "name": "dateOfBirth",
          "title": "Date of birth",
          "isRequired": true,
          "inputType": "date"
        },
        {
          "type": "text",
          "name": "email",
          "title": "Email",
          "isRequired": true,
          "inputType": "email",
          "description": "Exact email matches are checked against the registry on submission.",
          "validators": [{ "type": "email" }]
        },
        {
          "type": "text",
          "name": "phone",
          "title": "Phone",
          "placeholder": "e.g. +1 555 0100"
        },
        {
          "type": "text",
          "name": "participantId",
          "title": "External participant ID (optional)",
          "description": "If provided, exact ID matches are checked against the registry on submission.",
          "placeholder": "e.g. EXT-9001"
        }
      ]
    }
  ]
}
;
```

### `src/theme.js`

```js
export const themeJson = {};
```

### `package.json`

```json
{
  "dependencies": {
    "react": "latest",
    "react-dom": "latest",
    "survey-core": "latest",
    "survey-react-ui": "latest"
  },
  "devDependencies": {
    "react-scripts": "latest"
  }
}
```

## Other Frameworks

- [Angular](https://surveyjs.io/form-library/examples/detect-duplicate-form-records/angular.md)
- [Vue 3](https://surveyjs.io/form-library/examples/detect-duplicate-form-records/vue3js.md)
- [jQuery](https://surveyjs.io/form-library/examples/detect-duplicate-form-records/jquery.md)
- [Vanilla JS](https://surveyjs.io/form-library/examples/detect-duplicate-form-records/vanillajs.md)
