npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@updog/data-editor

v0.1.22

Published

Client-side CSV importer and spreadsheet editor SDK for React. Import CSV, Excel, JSON, TSV, and XML, match columns, validate, and edit 1M+ rows entirely in the browser.

Readme

@updog/data-editor

Client-side CSV importer and spreadsheet editor SDK for React. Your users import files, match columns to your schema, fix errors, and submit clean data. Edits happen inline, in the browser, at 1M+ rows.

What is @updog/data-editor

@updog/data-editor is a commercial React SDK that embeds a complete data import wizard and spreadsheet editor into your application.

  • Import CSV, Excel (XLSX), TSV, JSON, and XML — column auto-matching, value mapping, multi-source merging, validation
  • Edit 1M+ rows with undo/redo, find & replace, copy/paste, fill handle, sorting, filtering, and bulk transforms
  • Submit clean data through a single onComplete callback, classified into insert / update / delete

Everything runs client-side. Files are parsed, validated, and edited in the user's browser — no upload server, no data residency, no DPA required.

Not on React? The same SDK ships as a framework-agnostic Web Component — @updog/data-editor-wc — for Vue, Angular, Svelte, or vanilla JS.

Requirements

  • React 18.x or 19.x
  • An API key — this is a commercial SDK. Sign up at updog.tech to get one.

Installation

npm install @updog/data-editor

Quick Start

import { useState } from "react";
import { DataEditor } from "@updog/data-editor";
import "@updog/data-editor/styles.css";
import type { DataEditorColumn } from "@updog/data-editor";

type Employee = {
  id: string;
  name: string;
  email: string;
  role: string;
};

const columns: DataEditorColumn[] = [
  {
    id: "name",
    title: "Full Name",
    size: 200,
    validators: [{ type: "required", message: "Name is required" }],
  },
  {
    id: "email",
    title: "Email",
    size: 250,
    validators: [
      { type: "required", message: "Email is required" },
      { type: "email", message: "Invalid email" },
    ],
  },
  {
    id: "role",
    title: "Role",
    editor: { type: "select", options: ["Admin", "Editor", "Viewer"] },
  },
];

export function App() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Open Editor</button>
      <DataEditor<Employee>
        apiKey="your-license-key"
        open={open}
        onClose={() => setOpen(false)}
        columns={columns}
        primaryKey="id"
        loadData={async (onChunk) => {
          const res = await fetch("/api/employees");
          onChunk(await res.json());
        }}
        onComplete={async (result) => {
          for (const source of result.sources) {
            const inserts = source.rows.filter((r) => r.isNew && !r.isDeleted && r.isValid);
            const updates = source.rows.filter((r) => !r.isNew && r.isChanged && !r.isDeleted && r.isValid);
            const deletes = source.rows.filter((r) => r.isDeleted && !r.isNew);
            await persist(source.sourceId, { inserts, updates, deletes });
          }
          setOpen(false);
        }}
      />
    </>
  );
}

TypeScript

DataEditor accepts a generic type parameter for row data. This gives you type-safe access to primaryKey and the result rows in onComplete.

type Employee = { id: string; name: string; email: string };

<DataEditor<Employee>
  primaryKey="id"  // autocomplete shows "id" | "name" | "email"
  // ...
/>

When you omit the generic, rows are typed as Record<string, unknown>.

Inline Mode

Render the editor directly in the DOM without a modal overlay:

<DataEditor
  mode="inline"
  apiKey="your-license-key"
  columns={columns}
  primaryKey="id"
  loadData={loadData}
  onComplete={onComplete}
/>

In inline mode, open and onClose don't apply.

Props

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | apiKey | string | Yes | — | Your Updog license key. Validated on each open. | | columns | DataEditorColumn[] | Yes | — | Column definitions. | | primaryKey | keyof TRow | Yes | — | Column ID that uniquely identifies each row. | | mode | "modal" | "inline" | No | "modal" | Rendering mode. | | open | boolean | Modal only | — | Controlled modal visibility. | | onClose | () => void | Modal only | — | Called when the user closes the modal (X button or Escape). | | loadData | (onChunk) => Promise<void> | No | — | Async data loader. Stream rows in chunks, optionally tagged by source. | | onComplete | (result) => void \| Promise<void> | No | — | Called on submit. See Submission Result. | | variant | "editor" | "uploader" | No | "editor" | Initial view. "uploader" opens the import wizard first. | | translations | DataEditorTranslations | No | — | Partial i18n overrides. | | locale | string | No | "en" | BCP 47 locale tag. | | rtl | boolean | No | false | Right-to-left layout. | | readonly | boolean | No | false | Hide all editing UI. | | enableDeleteRow | "all" | "new" | false | No | false | Row deletion policy. | | enableAddRow | boolean | No | true | Show the "Add row" button. | | enableCreateColumn | boolean | No | true | Allow creating columns for unmatched CSV headers during import. | | importFormats | DataEditorFormat[] | No | all | Allowed import formats. [] disables import. | | exportFormats | DataEditorFormat[] | No | all | Allowed export formats. [] disables export. | | remoteSources | RemoteSource[] | No | — | Custom import buttons (Google Sheets, S3, etc.) rendered on the upload step. | | rowHeight | number | No | 34 | Row height in pixels. | | headerHeight | number | No | 36 | Header height in pixels. | | server | DataEditorServer<TRow> | No | — | Server-delegated mode: SDK renders, your backend handles queries and mutations. | | chat | DataEditorChat<TRow> | No | — | Bring-your-own AI chat panel. | | onColumnMatch | (headers, columns) => ... | No | — | Override import column matching. | | onValueMatch | (valuesToMatch) => ... | No | — | Override import value matching for select columns. | | synonyms | Record<string, string[]> | No | — | Extra synonyms for column auto-matching. | | sampleData | Record<string, unknown>[] | No | — | Rows used in the "Download Example" file. | | localStorage | false | { licenseGrant?: boolean } | No | { licenseGrant: true } | What the SDK caches in localStorage. | | onError | (error: UpdogError) => void | No | — | Called on internal errors. Use for logging or Sentry. | | className | string | No | — | CSS class on the wrapper element. |

Column Configuration

import type { DataEditorColumn } from "@updog/data-editor";

const columns: DataEditorColumn[] = [
  {
    id: "email",
    title: "Email",
    size: 260,

    // Declarative validators. See "Built-in Validators" for the full list.
    validators: [
      { type: "required", message: "Email is required" },
      { type: "email", message: "Enter a valid email" },
    ],

    // Flag duplicates in this column as errors.
    unique: true,

    // Revalidate these columns when this one changes.
    dependentFields: ["confirmEmail"],

    // Cell editor. Default is "text".
    editor: { type: "select", options: ["US", "UK", "DE"] },

    // Display-only formatting. Does not mutate stored data.
    formatter: (value) => value.toLowerCase(),

    // Runs when rows are uploaded to the editor. Mutates the stored value.
    transformer: (value) => String(value).trim(),

    // Sidebar filter control.
    filter: { type: "select" },

    // Lock cells in this column. `"default"` locks only default-source rows.
    locked: "default",
  },
];

Cell editors: { type: "text" } (default), { type: "date", minDate?, maxDate? }, { type: "select", options: string[] }, { type: "number", decimalPlaces?, decimalSeparator?, thousandsSeparator?, allowChars? }.

Column filters: { type: "select", label?, placeholder?, options?, multiple? }, { type: "number-range", label? }, { type: "date-range", label? }.

Built-in Validators

Validators are declarative objects passed in the validators array on each column. The SDK ships a TS interpreter for the client; Updog Scale ships a matching Go interpreter for server mode. Both runtimes are pinned to the same fixture set, so a column definition produces identical pass/fail results regardless of where it runs.

| type | Fields | Behavior | |---|---|---| | "required" | message? | Value must be non-empty. | | "email" | message? | Value must look like an email address. | | "numeric" | message? | Value must parse as a number. Commas are stripped before parsing. | | "regex" | pattern, flags?, message? | Value must match the regular expression. Empty values are skipped. | | "range" | min?, max?, message? | Value must be numerically within [min, max]. Either bound is optional. | | "oneOf" | values: string[], message? | Value must be one of the listed strings. | | "date" | format?: "YYYY-MM-DD" \| "DD/MM/YYYY", message? | Value must parse as a date in the given format. When format is omitted, either format is accepted. |

const columns: DataEditorColumn[] = [
  { id: "name", title: "Name", validators: [{ type: "required", message: "Required" }] },
  {
    id: "email",
    title: "Email",
    validators: [
      { type: "required", message: "Required" },
      { type: "email", message: "Invalid email" },
    ],
  },
  { id: "salary", title: "Salary", validators: [{ type: "numeric", message: "Must be a number" }] },
  {
    id: "status",
    title: "Status",
    validators: [{ type: "oneOf", values: ["active", "inactive"], message: "Invalid status" }],
  },
  {
    id: "startDate",
    title: "Start",
    validators: [{ type: "date", format: "YYYY-MM-DD", message: "Use YYYY-MM-DD" }],
    editor: { type: "date" },
    dependentFields: ["endDate"],
  },
];

A ValidationError with level: "error" flags the cell in the grid but does not block submission — invalid rows are delivered to onComplete tagged via isValid: false.

Custom validators

For cross-field checks or anything not covered by the built-ins, wrap a CellValidator function in { type: "function", fn }. Function validators are client-mode only — they're dropped (with a one-time warning) when the SDK serializes the schema for server mode. Use dependentFields on the column to trigger re-validation when another column changes.

import type { CellValidator, DataEditorColumn } from "@updog/data-editor";

const endAfterStart: CellValidator = (value, row) => {
  if (!value || !row.startDate) return null;
  if (String(value) < String(row.startDate)) {
    return { level: "error", message: "End must be on or after start" };
  }
  return null;
};

const columns: DataEditorColumn[] = [
  {
    id: "endDate",
    title: "End",
    editor: { type: "date" },
    validators: [
      { type: "date", message: "Invalid date" },
      { type: "function", fn: endAfterStart },
    ],
  },
];

The only level is "error". Return null to indicate the value is valid.

Data Loading

loadData is called once when the editor opens. Call onChunk one or more times to stream rows. The grid renders each chunk without blocking.

// Single source
loadData={async (onChunk) => {
  for (let page = 0; page < totalPages; page++) {
    const rows = await fetch(`/api/employees?page=${page}`).then((r) => r.json());
    onChunk(rows);
  }
}}

// Multiple sources — each chunk tagged with a source
loadData={async (onChunk) => {
  onChunk(await fetchSalesforce(), { source: "Salesforce", done: true });
  onChunk(await fetchHubSpot(), { source: "HubSpot", deletable: true, done: true });
}}

ChunkSourceOptions: { source: string; id?: string; deletable?: boolean; done?: boolean }. Sources are auto-registered on first encounter. Chunks without options go to "Existing Data". When loadData resolves, any source still loading is finalized automatically.

Submission Result

onComplete(result) fires when the user submits. May return a promise.

type DataEditorResult<TRow> = {
  sources: {
    sourceId: string;
    sourceName: string;
    rows: {
      row: TRow;
      isNew: boolean;      // imported or manually added this session
      isChanged: boolean;  // cell values differ from origin
      isDeleted: boolean;  // marked for deletion
      isValid: boolean;    // passes all validators
    }[];
  }[];
  counts: { new: number; changed: number; deleted: number; invalid: number };
};

Pristine backend rows (unchanged, not deleted, not new) are omitted from sources[].rows — they're no-ops. The flags are orthogonal: a row can be new, changed, and deleted at once. You own the routing rules:

onComplete={async (result) => {
  for (const source of result.sources) {
    const inserts = source.rows.filter((r) => r.isNew && !r.isDeleted && r.isValid);
    const updates = source.rows.filter((r) => !r.isNew && r.isChanged && !r.isDeleted && r.isValid);
    const deletes = source.rows.filter((r) => r.isDeleted && !r.isNew);
    await persist(source.sourceId, { inserts, updates, deletes });
  }
}}

If you never tagged sources via loadData, you'll get one entry with sourceId: "backend". result.counts is an aggregate for summary UI — route with the per-row flags, not counts.

Import Configuration

The import wizard guides users through uploading a file, mapping columns to your schema, and resolving value mismatches.

Formats

Supported: "csv", "tsv", "xlsx", "json", "xml".

<DataEditor
  importFormats={["csv", "xlsx"]} // only CSV and XLSX
  exportFormats={[]}              // disable export entirely
/>

Override column matching

onColumnMatch={async (headers, columns) => {
  const mappings = await myMatchingService.match(headers, columns);
  return mappings; // { csvHeader: columnId | null }
}}

Return a map of { csvHeader: columnId | null }. Entries set to null or omitted fall back to built-in matching.

Override value matching

For select columns, override imported-value → option mapping:

onValueMatch={async (valuesToMatch) => {
  // valuesToMatch = { country: { importedValues: ["espana", "fr"], options: ["Spain", "France"] } }
  return {
    country: { espana: "Spain", fr: "France" },
  };
}}

Values set to null skip auto-matching. Unmapped values fall back to built-in fuzzy matching.

Synonyms

Extra aliases for the built-in column matcher:

synonyms={{
  productSku: ["sku", "article_no", "item_code"],
  firstName: ["first", "given_name", "fname"],
}}

Remote sources

Render custom import buttons for third-party sources. You own auth and picker logic; the SDK renders the button and processes the result.

const googleSheets: RemoteSource = {
  id: "google-sheets",
  label: "Google Sheets",
  icon: "<svg>...</svg>",
  fetch: async () => {
    const data = await myGoogleSheetsLib.pick();
    return data.rows; // Record<string, unknown>[] — or return a File
  },
};

<DataEditor remoteSources={[googleSheets]} {...rest} />

Return a File to parse via the standard CSV/XLSX/JSON/XML pipeline, or return structured records to skip parsing.

Error Handling

Log SDK-internal errors through onError. The SDK recovers gracefully — use this for monitoring, not recovery.

onError={(error) => {
  Sentry.captureException(error.originalError ?? error, {
    tags: { code: error.code, source: error.source },
  });
}}

Error codes: PARSE_ERROR, RENDER_ERROR, TRANSFORM_ERROR, VALIDATION_ERROR, WORKER_ERROR, COMMAND_ERROR, OPERATION_ERROR.

Non-React Usage

For Angular, Vue, Svelte, or vanilla JS use the Web Component build:

npm install @updog/data-editor-wc

See @updog/data-editor-wc.

FAQ

Does my user's data leave the browser? No. Parsing, validation, mapping, and editing all run client-side. Your code controls when (and whether) data is submitted to your own backend.

Is there a row limit? No hard cap. The grid is engineered for 1M+ rows using canvas rendering and virtual scrolling.

Are there per-import or per-row fees? No. Pricing is flat per production domain. See updog.tech/#pricing.

Can I white-label? Yes, on every plan including the free tier. Strip all Updog branding via CSS theming.

Which file formats are supported? CSV, Excel (XLSX), TSV, JSON, and XML on both import and export.

License

Commercial — see LICENSE for the full terms of the Updog SDK Commercial License v1.0. Contact [email protected] for enterprise licensing. Third-party dependencies are listed in THIRD_PARTY_NOTICES.txt.