Skip to content

feat: add jsonToHtmlAsync and jsonToReact output modes#100

Draft
glassdimlygr wants to merge 2 commits intocontentstack:masterfrom
glassdimlygr:feat/async-json-to-html
Draft

feat: add jsonToHtmlAsync and jsonToReact output modes#100
glassdimlygr wants to merge 2 commits intocontentstack:masterfrom
glassdimlygr:feat/async-json-to-html

Conversation

@glassdimlygr
Copy link
Copy Markdown

@glassdimlygr glassdimlygr commented May 7, 2026

Summary

Adds two new output modes to the serializer, both purely additive — existing jsonToHtml / htmlToJson / jsonToMarkdown are untouched.

  1. jsonToHtmlAsync — async variant of jsonToHtml where customElementTypes handlers can return Promise<string>
  2. jsonToReact — React element tree output mode where handlers return ReactNode directly

Both modes are opt-in: consumers import the one they need.


jsonToHtmlAsync

Motivation

Frameworks like Next.js use dynamic imports (next/dynamic, React.lazy) for code-splitting. When rendering JSON RTE content that contains embedded component references, the component modules may not be available synchronously. Currently, jsonToHtml requires all handlers to return strings synchronously, making it impossible to await import() a component before rendering it.

Changes

  • toRedactorAsync (exported as jsonToHtmlAsync): Async version of toRedactor. Children are resolved concurrently via Promise.all. Handler results are awaited, so both sync (string) and async (Promise<string>) return values work.
  • IJsonToHtmlAsyncElementTags: Handler signature where return type is string | Promise<string>
  • IJsonToHtmlAsyncOptions: Options interface using the async element tags type

Usage

import { jsonToHtmlAsync } from '@contentstack/json-rte-serializer';

const html = await jsonToHtmlAsync(jsonRteContent, {
  customElementTypes: {
    // Sync handlers still work
    'p': (attrs, child) => `<p${attrs}>${child}</p>`,
    
    // Async handlers now supported
    'reference': async (attrs, child, jsonBlock) => {
      const Component = await import(`./components/${jsonBlock.attrs.type}`);
      return renderToStaticMarkup(<Component {...jsonBlock.attrs} />);
    },
  },
});

jsonToReact

Motivation

jsonToHtml serializes JSON RTE to an HTML string, which then needs dangerouslySetInnerHTML to render in React. This has several downsides:

  • No live React components: Embedded references become static HTML — they can't use hooks, context, Suspense, or participate in the React lifecycle
  • renderToStaticMarkup limitations: Can't resolve lazy/dynamic imports on the client, breaks hydration
  • Security: dangerouslySetInnerHTML bypasses React's XSS protections

jsonToReact returns a real React element tree instead. Custom handlers receive the JSON block and pre-rendered children as ReactNode, and return ReactNode directly.

Changes

  • toReactTree.tsx (exported as jsonToReact): Walks the JSON RTE document and composes React elements. Default handlers map all standard types to JSX equivalents (p<p>, h1<h1>, a<a>, etc.)
  • IJsonToReactElementHandler: (jsonBlock, children: ReactNode) => ReactNode — cleaner than the string mode signature since handlers get the full JSON block (no pre-serialized attrs string) and real React children
  • IJsonToReactTextHandler: (children: ReactNode, value?) => ReactNode — for text marks (bold, italic, etc.)
  • IJsonToReactOptions: { customElementTypes?, customTextMarks? }
  • React as optional peer dependency: react >= 16 in peerDependencies with optional: true. Externalized from the esbuild bundle so it's not bundled into the output.

Handler signature comparison

// jsonToHtml handler (string mode)
(attrs: string, child: string, jsonBlock: any) => string

// jsonToReact handler (React mode)
(jsonBlock: any, children: ReactNode) => ReactNode

Usage

import { jsonToReact } from '@contentstack/json-rte-serializer';

const content = jsonToReact(jsonRteContent, {
  customElementTypes: {
    reference: (jsonBlock, children) => {
      const contentType = jsonBlock.attrs?.['content-type-uid'];
      return <MyComponent type={contentType} />;
    },
    a: (jsonBlock, children) => {
      return <Link to={jsonBlock.attrs?.url}>{children}</Link>;
    },
  },
});

// Render directly — no dangerouslySetInnerHTML needed
return <div className="wysiwyg">{content}</div>;

What's NOT changed

  • jsonToHtml / toRedactor — completely untouched
  • htmlToJson / fromRedactor — completely untouched
  • jsonToMarkdown — completely untouched
  • All existing tests pass without modification
  • No breaking changes to any existing API

Tests

jsonToHtmlAsync

  • Full parity tests: produces identical output to jsonToHtml for all existing test cases
  • Async-specific tests: async handlers, mixed sync/async, concurrent resolution, error propagation

jsonToReact

  • Build verification: esbuild CJS + ESM bundles compile, TypeScript declarations emit correctly
  • React externalized: not bundled, resolved from consumer's node_modules

@glassdimlygr glassdimlygr force-pushed the feat/async-json-to-html branch from 6cbbd2d to c2200a4 Compare May 7, 2026 15:14
Add toRedactorAsync (exported as jsonToHtmlAsync) that supports
customElementTypes handlers returning string | Promise<string>.
Enables dynamic component resolution (e.g. await import()) before
serialization. Children are resolved via Promise.all concurrently.

Refactors shared logic (text processing, attr building, element
node processing) into toRedactorHelpers.ts so both sync and async
versions are thin recursive shells with no duplicated code.

The existing sync jsonToHtml behavior is unchanged.

New types: IJsonToHtmlAsyncElementTags, IJsonToHtmlAsyncOptions.
@glassdimlygr glassdimlygr force-pushed the feat/async-json-to-html branch from c2200a4 to 44da5fa Compare May 7, 2026 15:56
Add toReactTree.tsx which walks the JSON RTE document and returns
ReactNode instead of HTML strings. Handlers receive (jsonBlock, children)
and return ReactNode directly, enabling real React component rendering
without renderToStaticMarkup.

- New exports: jsonToReact, IJsonToReactOptions, IJsonToReactElementHandler, IJsonToReactTextHandler
- React is a peer dependency (optional) and externalized from the bundle
- Default handlers map all standard element types to JSX equivalents
- Text mark handlers (bold, italic, etc.) wrap children in semantic elements
@glassdimlygr glassdimlygr changed the title feat: add jsonToHtmlAsync for async customElementTypes support feat: add jsonToHtmlAsync and jsonToReact output modes May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant