json-edit-react

repository·main·Indexed 20 days ago

https://github.com/carlosnz/json-edit-react

A highly-configurable React component for viewing and editing JSON/object data. Version 2.0.0-beta.8 features inline editing, granular access control, JSON Schema validation, and a customizable UI. It includes a companion library, @json-edit-react/components, providing specialized node components for data types such as Hyperlinks, DatePickers, ColorPickers, Markdown, BigInt, and NaN, as well as editor slot widgets like ReactSelect and CodeEditor.

Tokens
76.1K
Snippets
210
Records
299
Agent score
69%

What's inside json-edit-react

  1. Overview of the four packages

    main

    The project is a multi-package workspace consisting of four independent packages:

    • json-edit-react: Located at the repo root. Contains the core editor component and primary types.
    • @json-edit-react/utils: Located in packages/utils/. Contains utility hooks and helpers like useConfirmOnUpdate and useUndo.
    • @json-edit-react/themes: Located in packages/themes/. Contains pre-built theme objects.
    • @json-edit-react/components: Located in packages/components/. Contains ready-to-use custom node components.
  2. Understand the Editing Model v2 Architecture

    main

    The json-edit-react editing model is organized into three distinct layers to manage data mutations and UI states:

    1. JsonEditor (Data Owner): Owns the raw data and the onUpdate prop. It provides stable mutation primitives via refs, including runUpdate (the onUpdate callback), applyValue(path, val), getLatestData, and buildNodeData.
    2. EditingProvider (Control Center / Commit Engine): Manages the active/held operation state and the settlements registry. It handles the lifecycle of an edit (open → submit → cancel), runs the optimistic-to-settle cycle, and is the single source of truth for all onEditEvent emissions. It exposes selectors like isEditing, isEditingKey, isAddingHere, isHeld, and isSettling.
    3. Nodes (UI Layer): Components like ValueNodeWrapper or CollectionNode that own a local buffer (the user's current input) decoupled from the main data. They subscribe to the Provider's selectors and trigger transitions like open, submit, or cancel.
    JsonEditor (Data) <──> EditingProvider (Control) <──> Nodes (UI/Buffer)
  3. Ensure Referential stability when using filters

    main

    The allow* and searchFilter props are compared by identity at the React.memo boundary. To prevent unnecessary re-renders, you should avoid passing fresh arrow functions directly to these props.

    The Toolkit Advantage: The builders in @json-edit-react/utils/filters are interned. This means byKey('name') called during render $N$ will return the exact same function instance as it did during render $N-1$, making it safe to use inline. Combinators also intern based on their children's identities.

    Warning: If you use a raw inline function inside a combinator, like and(byKey('id'), (node) => node.value === 0), the and function will receive a new identity every render and cannot be cached. In this case, you must hoist the entire expression or the inline function using useMemo or module scope.

  4. Performance optimization in v2.0

    main

    The json-edit-react v2.0 release introduced significant performance improvements for large JSON trees by moving from a recursive re-render model to a fine-grained, subscription-based model.

    Key architectural changes include:

    • External Store via useSyncExternalStore: Editing state is no longer passed through a standard React Context value that triggers global re-renders. Instead, nodes subscribe to a stable external store, meaning an edit only re-renders the specific node being edited and its ancestor spine. This requires React 18 or higher.
    • Lazy jsonStringify: Collection buffers are computed on demand when entering edit mode, rather than eagerly on mount.
    • Memoization: CollectionNode and ValueNodeWrapper use React.memo with a custom comparator. This ensures that sibling subtrees that are not part of the current edit path bail out of re-renders based on data reference equality.
    • Complexity: Re-render cost is now O(edited node + its ancestor spine), making performance independent of the total tree size.

    Note on Large Trees: For extremely large, fully-expanded trees (e.g., >19k nodes), the bottleneck shifts from React re-renders to DOM size and native HTML5 drag-and-drop limitations. Virtualization/windowing is planned for version 2.x.

  5. Use Enums to restrict values

    main

    Enums allow you to restrict a node's value to a pre-defined list of options. To define an Enum, include an object in your types array (passed to restrictTypeSelection) with this structure:

    • enum: The name of the Enum type (displayed in the selector).
    • values: An array of allowed values.
    • matchPriority (Optional): An integer used to automatically recognize existing string values as this Enum type during initialization. If multiple Enums have overlapping values, the one with the highest matchPriority is assigned.

    Note: Once an Enum is selected via the UI, it remains the selected type for that node for the rest of the session.

    // Defining Enums within restrictTypeSelection
    restrictTypeSelection = [
      'string', 
      'number', 
      {
        enum: 'Weekday',
        values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
        matchPriority: 1,
      },
      {
        enum: 'Colour',
        values: ['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Indigo', 'Violet'],
        matchPriority: 1,
      },
    ]
  6. Understand path identity and toPathString encoding

    main

    The library has moved away from dot-joined strings for node identity to avoid ambiguity when keys contain dots. The canonical node identity is now a CollectionKey[] (an array of keys).

    If you use the toPathString utility, be aware that the encoding has changed to use / and encodeURIComponent. This ensures that paths are injective and safe. The second argument 'key_' has been removed as the mode is now handled as a separate field.

  7. Manage optimistic commits with the `hold` function

    main

    By default, onUpdate is optimistic: the editor closes and data updates immediately, then onUpdate runs in the background. If onUpdate rejects, the change is reverted.

    To prevent the editor from closing immediately (e.g., to show a confirmation dialog), use the second argument of onUpdate to call hold(). This blocks the tree and keeps the editor open. You must call the returned release() function synchronously to commit the change or abort it.

    onUpdate={async (props, { hold }) => {
      const release = hold(); // keep the editor open + block the tree
      const ok = await confirmDialog(props);
      if (!ok) return null; // abort
      release();            // commit now
    }} 
  8. Understand benchmark metrics: mount, update, and interactions

    main

    The benchmark suite measures three distinct types of work to evaluate how the editor scales:

    • mount: Renders the entire (fully-expanded) tree once. This measures the headline size-scaling cost and exposes per-leaf wrapper overhead.
    • update: Forces a whole-tree re-render by providing a fresh data clone where every node's identity has changed. This is the update-path equivalent of mount and provides the cleanest per-node signal.
    • interactions: Drives real user actions like enter-edit, commit, or tab-move on a specific leaf and times the resulting commit. This confirms that per-edit costs remain small (ideally $O(\text{edited node} + \text{spine})$) regardless of tree size.
  9. Understand Demo resolution modes (VITE_JRE_SOURCE)

    main

    The demo's Vite configuration uses the VITE_JRE_SOURCE environment variable to alias package imports to different locations. This allows for different testing scenarios:

    ModeResolution Target
    localResolves to raw source files (e.g., @json-edit-react../src/). Best for active development.
    buildResolves to the build/ output directory of each package.
    packResolves to extracted tarballs in pack-output/<name>/package/. This is the closest simulation of a real npm install.
    npmFalls back to whatever is currently in node_modules.

    Note on pack mode: To use this, run pnpm pack-all first. This mode is useful for catching packaging errors (missing files, bad exports maps, etc.). It strips peerDependencies from the extracted package.json to prevent workspace resolution failures, so you must manually verify peerDependencies correctness.

  10. How the `dialog` object works for modal integration

    main

    The dialog object returned by both useConfirmOnUpdate and useJsonEditorConfirm is the contract for wiring up your own UI (modal, popover, or plain div).

    Properties of the dialog object:

    • isOpen: A boolean controlling the visibility of your modal.
    • onConfirm: The handler to call when the user clicks the confirmation button.
    • onCancel: The handler to call when the user clicks the cancel button.
    • title: The title string for the dialog.
    • message: The message string for the dialog.

    When using confirm() in the low-level hook, any additional keys passed to the confirm method will be included in the dialog object.

  11. Themeable vs Fixed-colour glyphs

    main

    You can control how icons respond to theme color changes via the SVG fill attribute:

    • Themeable glyphs: Set the SVG paths to use fill="currentColor". These will automatically adopt the color provided by the theme's styles.icon[PascalCaseName] entry.
    • Fixed-colour glyphs: Hardcode a specific color in the path (e.g., <path fill="#f00" ... />). These paths will ignore the theme's color injection. This is useful for multi-color brand logos or flags.
  12. Invariants of the Settlement Mechanism

    main

    To ensure data integrity during asynchronous updates, the following rules apply:

    1. Buffer Isolation: While an editing session is open, the editor's local buffer is independent. External changes to the data prop will not resync or clobber an open editor.
    2. Token-Gated Reverts: When a settlement resolves, it only reverts or emits an error if it is still the current token for that path. If a newer commit has already started, the old settlement is silently superseded.
    3. Optimistic Rollback: On failure, the system reverts to the last optimistic value (the state immediately after the user's input was applied), not the last confirmed value from the server/source.