---
title: "Structured Text Format"
description: "The Structured Text Format (stf) package provides a specialized data engine and reactive subscription model designed for interactive schema binding and state management within Fumadocs. It anchors ..."
last_updated: "2026-10-12T07:53:59.736226+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/developer-tooling/structured-text-format"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/core/src/mdx-plugins/remark-structure.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-structure.ts)
- [packages/local-md/src/client.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/client.ts)
- [packages/core/src/mdx-plugins/remark-llms.runtime.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-llms.runtime.ts)
- [packages/stf/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/index.ts)
- [packages/stf/src/lib/stf.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx)
- [packages/core/src/source/client/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx)
- [packages/story/src/utils/serialization.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/story/src/utils/serialization.ts)
- [packages/twoslash/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts)
- [packages/core/src/content/md.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/content/md.ts)
- [packages/python/src/convert.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/src/convert.ts)
- [packages/mdx/src/runtime/server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/server.ts)
- [packages/doc-gen/src/remark-docgen.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/doc-gen/src/remark-docgen.ts)
- [packages/core/src/mdx-plugins/remark-llms.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-llms.ts)
- [packages/asyncapi/src/ui/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/ui/index.tsx)
- [packages/api-docs/src/components/schema/client.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/components/schema/client.tsx)
- [packages/openapi/src/ui/base.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/ui/base.tsx)
- [packages/base-ui/src/components/codeblock.rsc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/codeblock.rsc.tsx)
- [packages/typescript/src/markdown.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/typescript/src/markdown.ts)
- [packages/core/src/mdx-plugins/stringifier.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/stringifier.ts)
- [packages/python/src/components/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/src/components/index.tsx)
- [packages/mdx/src/loaders/mdx/remark-include.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/loaders/mdx/remark-include.ts)
- [packages/preview/src/components/markdown.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/markdown.tsx)
- [packages/asyncapi/src/utils/schema.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/schema.ts)
- [packages/sanity/registry/steps.component.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/steps.component.tsx)
- [packages/radix-ui/src/components/codeblock.rsc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/codeblock.rsc.tsx)
- [packages/python/fumapy/mksource/document_module.py](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/fumapy/mksource/document_module.py)
- [packages/local-md/src/md/renderer.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/renderer.ts)
- [packages/api-docs/src/schema/to-string.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/schema/to-string.ts)
- [packages/content-collections/src/configuration.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content-collections/src/configuration.ts)
- [packages/asyncapi/src/utils/pages/to-text.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/to-text.ts)
</details>

## Overview

The Structured Text Format (`stf`) package provides a specialized data engine and reactive subscription model designed for interactive schema binding and state management within Fumadocs. It anchors client-side form controls and schema editors by maintaining nested hierarchical values, tracking field-level modifications, and publishing reactive update notifications to subscribed components.

Sources: [packages/stf/src/index.ts:1-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/index.ts#L1-L4), [packages/stf/src/lib/stf.tsx:38-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L38-L42), [packages/stf/src/lib/stf.tsx:190-237](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L190-L237)

## Data Engine Architecture and Reactive State

### Call-Chain Execution Walkthrough

The core data structures and listener subscription model in `DataEngine` manage state propagation and reactivity across hierarchical fields. React hooks bridge component rendering cycles with the underlying data instance by subscribing to field-level mutations.

Sources: [packages/stf/src/lib/stf.tsx:38-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L38-L42), [packages/stf/src/lib/stf.tsx:101-188](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L101-L188), [packages/stf/src/lib/stf.tsx:190-237](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L190-L237)

The traversal from a high-level component hook down to the core data instance flows through a deterministic sequence of function calls:
1. `useObject` initiates structured property inspection for a given field key.
2. `useFieldValue` wraps the field access, setting up computation and change detection.
3. `useListener` establishes the lifecycle bindings and registers event handlers.
4. `useDataEngine` resolves the active `DataEngine` instance from the provided argument or React context.

Sources: [packages/stf/src/lib/stf.tsx:38-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L38-L42), [packages/stf/src/lib/stf.tsx:101-128](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L101-L128), [packages/stf/src/lib/stf.tsx:190-237](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L190-L237), [packages/stf/src/lib/stf.tsx:239-258](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L239-L258)

```mermaid
sequenceDiagram
    participant useObject as useObject (stf.tsx)
    participant useFieldValue as useFieldValue (stf.tsx)
    participant useListener as useListener (stf.tsx)
    participant useDataEngine as useDataEngine (stf.tsx)

    useObject->>useFieldValue: calls with field & options
    useFieldValue->>useListener: registers field hooks
    useListener->>useDataEngine: resolves engine via useDataEngine()
```

Sources: [packages/stf/src/lib/stf.tsx:38-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L38-L42), [packages/stf/src/lib/stf.tsx:101-128](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L101-L128), [packages/stf/src/lib/stf.tsx:190-237](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L190-L237), [packages/stf/src/lib/stf.tsx:239-258](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L239-L258)

> [!NOTE]
> When `instance` is passed directly to `useDataEngine`, it bypasses the React context lookup entirely, supporting standalone or multi-engine configurations.

Sources: [packages/stf/src/lib/stf.tsx:38-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L38-L42)

## Cross-Boundary Payload Serialization and Deserialization

### Payload Serialization and Recursive JSON Handling

To safely transmit complex data structures across boundaries, Fumadocs utilizes specialized serialization routines. The `@ungap/structured-clone/json` package provides deep parsing and stringification capabilities capable of handling recursive and JSON-compatible data models.

```typescript
export function serialize(value: unknown): string {
  return stringify(value);
}

export function deserialize(value: string): unknown {
  return parse(value);
}
```
Sources: [packages/story/src/utils/serialization.ts:4-10](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/story/src/utils/serialization.ts#L4-L10)

> [!NOTE]
> The `serialize` and `deserialize` utility functions operate directly on unknown types, enabling transparent round-tripping of complex payloads without losing recursive references.

Sources: [packages/story/src/utils/serialization.ts:1-10](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/story/src/utils/serialization.ts#L1-L10)

### Page Tree Deserialization and Client Hydration

Server-side loaders in Fumadocs produce serialized representations containing page tree configurations tagged with specific marker identifiers. The client inspects these payloads via `isSerializedPageTree`, verifying the engine identifier before running structural hydration.

```tsx
function isSerializedPageTree(v: unknown): v is SerializedPageTree {
  return (
    typeof v === 'object' &&
    v !== null &&
    '$fumadocs_loader' in v &&
    v.$fumadocs_loader === 'page-tree'
  );
}
```
Sources: [packages/core/src/source/client/index.tsx:62-69](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L62-L69)

During deserialization, `deserializePageTree` processes the root node by visiting each item recursively to hydrate raw HTML strings for item names and icons into React elements using `dangerouslySetInnerHTML`.

```tsx
export function deserializePageTree(serialized: SerializedPageTree): PageTree.Root {
  const root = serialized.data as PageTree.Root;
  visit(root, (item) => {
    if ('icon' in item && typeof item.icon === 'string') {
      item.icon = (
        <span
          dangerouslySetInnerHTML={{
            __html: item.icon,
          }}
        />
      );
    }
    if (typeof item.name === 'string') {
      item.name = (
        <span
          className="fd-page-tree-item-name"
          dangerouslySetInnerHTML={{
            __html: item.name,
          }}
        />
      );
    }
  });

  return root;
}
```
Sources: [packages/core/src/source/client/index.tsx:14-39](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L14-L39)

> [!WARNING]
> Icon and name strings found within page tree items are transformed into raw markup wrapper spans via `dangerouslySetInnerHTML`; ensure server-provided strings are sanitized before serialization.

Sources: [packages/core/src/source/client/index.tsx:16-35](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L16-L35)

The `useFumadocsLoader` hook memoizes the deserialization process across component lifecycles, scanning input properties and automatically decoding recognized loader formats while preserving all unrelated properties in the output object.

```tsx
export function useFumadocsLoader<V>(serialized: V): Serialized<V> {
  return useMemo(() => {
    const out: Record<string, unknown> = {};
    for (const k in serialized) {
      const v = serialized[k];
      if (isSerializedPageTree(v)) {
        out[k] = deserializePageTree(v);
      } else {
        out[k] = v;
      }
    }
    return out as Serialized<V>;
  }, [serialized]);
}
```
Sources: [packages/core/src/source/client/index.tsx:47-60](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L47-L60)

## Unified Stringification and Structural Transformations

### Stringification Options and AST Customization

Fumadocs utilizes a unified stringifier configuration built on top of `mdast-util-to-markdown` and `mdast-util-mdx` to serialize Abstract Syntax Tree nodes into plain text or formatted markdown. The stringification engine accepts configuration options controlling element inclusion, attribute filtering, and custom node rendering.

| Option | Type | Default | Purpose |
| :--- | :--- | :--- | :--- |
| `filterElement` | `(node: Nodes) => boolean \| 'children-only'` | `File`, `TypeTable`, `Callout`, `Card` return `true`; other MDX elements return `'children-only'`; all other nodes return `true` | Determines whether to include an element, drop it, or preserve only its children. |
| `filterMdxAttributes` | `(node, attribute) => boolean` | `undefined` | Filters individual attributes on MDX flow and text elements during stringification. |
| `stringify` | `(node, parent, state, info, ctx) => string \| undefined` | `undefined` | Overrides the default stringification logic for specific nodes. |

Sources: [packages/core/src/mdx-plugins/stringifier.ts:34-83](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/stringifier.ts#L34-L83), [packages/core/src/mdx-plugins/stringifier.ts:94-114](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/stringifier.ts#L94-L114)

> [!NOTE]
> Nodes can carry extra metadata via `node.data._stringify`, which supports three values: `'children-only'`, `{ node }` to replace the node entirely during serialization, or `{ text }` to return a pre-computed string form directly.

Sources: [packages/core/src/mdx-plugins/stringifier.ts:18-28](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/stringifier.ts#L18-L28), [packages/core/src/mdx-plugins/stringifier.ts:126-135](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/stringifier.ts#L126-L135)

### Placeholder Rendering and Docgen Transformers

The `renderPlaceholder` function evaluates control character placeholders delimited by null bytes (`\0`) within markdown or text strings, asynchronously resolving structured JSON payloads and rendering child nodes recursively.

```typescript
export async function renderPlaceholder(
  text: string,
  renderers: Record<string, (data: PlaceholderData) => Awaitable<string>>,
): Promise<string> {
  let out = '';
  let idx = 0;

  for (const match of text.matchAll(regex)) {
    out += text.slice(idx, match.index);
    const inner = match[1];
    try {
      const data = JSON.parse(inner) as PlaceholderData;
      const renderer = data.name && renderers[data.name];
      if (data.children.trim()) {
        data.children = await renderPlaceholder(data.children, renderers);
      }

      if (renderer) {
        out += await renderer(data);
      } else {
        out += data.children;
      }
    } catch {
      out += match[0];
    }

    idx = match.index + match[0].length;
  }
  out += text.slice(idx);

  return out;
```
Sources: [packages/core/src/mdx-plugins/remark-llms.runtime.ts:12-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-llms.runtime.ts#L12-L42)

> [!CAUTION]
> If JSON parsing fails on the inner content of a `\0...\0` placeholder, the parser catches the error and silently appends the raw match string back into the output without modification.

Sources: [packages/core/src/mdx-plugins/remark-llms.runtime.ts:22-36](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-llms.runtime.ts#L22-L36)

Additionally, `remarkDocGen` transforms code blocks matching specific docgen generators by visiting `code` nodes where the language is `json` and the meta string matches the `doc-gen:<name>` pattern.

```typescript
export function remarkDocGen({ generators = [] }: RemarkDocGenOptions): Transformer<Root, Root> {
  return async (tree, file) => {
    generators.forEach((gen) => gen.onFile?.(tree, file));
    const queue: Promise<void>[] = [];

    visit(tree, 'code', (code, _, parent) => {
      if (code.lang !== 'json' || !code.meta || !parent) return;

      const matches = metaRegex.exec(code.meta);
      if (!matches) return;

      const name = matches[1];
      const gen = generators.find((g) => g.name === name);

      const run = async (): Promise<void> => {
        const result = await gen?.run(JSON.parse(code.value), {
          cwd: file.cwd,
          path: file.path,
          node: code,
        });
        const index = parent.children.findIndex((c) => c === code);

        if (result && index !== -1) {
          const items = Array.isArray(result)
            ? (result as BlockContent[])
            : [result as BlockContent];

          parent.children.splice(index, 1, ...items);
        }
      };

      queue.push(run());
    });

    await Promise.all(queue);
  };
```
Sources: [packages/doc-gen/src/remark-docgen.ts:31-66](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/doc-gen/src/remark-docgen.ts#L31-L66)

## Client Field Hooks and Form Bindings

### React Context and Engine Integration

The structured text format framework supplies React bindings through `StfProvider`, `useStf`, and `useDataEngine` to expose data engines across component subtrees. `StfProvider` accepts an `Stf` instance containing a `DataEngine` and wraps child elements using React's context provider. `useStf` accepts configuration options with `defaultValues` and instantiates a memoized `DataEngine`.

```tsx
const Context = createContext<Stf | null>(null);

export interface Stf {
  dataEngine: DataEngine;
}

export function StfProvider({ value, children }: { value: Stf; children: ReactNode }) {
  return <Context value={value}>{children}</Context>;
}

export function useStf(options: {
  /**
   * Note: the passed object will be modified in place, use `structuredClone()` to keep the original object unchanged.
   */
  defaultValues?: DefaultValue<Record<string, unknown>>;
}): Stf {
  const optionsRef = useRef(options);
  optionsRef.current = options;

  return useMemo(
    () => ({
      dataEngine: new DataEngine(optionsRef.current.defaultValues),
    }),
    [],
  );
}

export function useDataEngine(instance?: Stf | DataEngine) {
  if (instance instanceof DataEngine) return instance;
  if (instance) return instance.dataEngine;
  return use(Context)!.dataEngine;
}
```
Sources: [packages/stf/src/lib/stf.tsx:11-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L11-L42)

> [!NOTE]
> When `useStf` is called, the passed `defaultValues` object is modified in place during engine initialization; callers requiring original object preservation should pass a clone via `structuredClone()`.

Sources: [packages/stf/src/lib/stf.tsx:21-25](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L21-L25)

`useDataEngine` resolves an active `DataEngine` instance by inspecting an explicitly passed instance or falling back to the React context.

```tsx
export function useDataEngine(instance?: Stf | DataEngine) {
  if (instance instanceof DataEngine) return instance;
  if (instance) return instance.dataEngine;
  return use(Context)!.dataEngine;
}
```
Sources: [packages/stf/src/lib/stf.tsx:38-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L38-L42)

### Field and Listener Hooks

Field bindings rely on `useFieldValue` and `useListener` to subscribe components to specific data engine paths, re-rendering when values initialize, update, or delete. `useFieldValue` accepts a field key, an optional `stf` reference, a `defaultValue`, a `compute` transformation function, and an `isChanged` comparator.

```tsx
export function useFieldValue<V = unknown>(
  key: FieldKey,
  options: {
    stf?: Stf | DataEngine;
    defaultValue?: DefaultValue;

    /**
     * compute value from the actual field value.
     *
     * to re-compute on in-place updates (may happen on objects, arrays), you should clone the object here.
     */
    compute?: (currentValue: unknown) => V;
    /** determine whether the value/computed value is changed */
    isChanged?: (prev: V, next: V) => boolean;
  } = {},
) {
  const { stf, compute = (v) => v as V, defaultValue, isChanged = (a, b) => a !== b } = options;
  const engine = useDataEngine(stf);
  const [value, setValue] = useState<V>(() =>
    compute(defaultValue === undefined ? engine.get(key) : engine.init(key, defaultValue)),
  );
  const prevEngineRef = useRef(engine);

  if (prevEngineRef.current !== engine) {
    setValue(
      compute(defaultValue === undefined ? engine.get(key) : engine.init(key, defaultValue)),
    );
    prevEngineRef.current = engine;
  }

  function onUpdate() {
    const computed = compute(engine.get(key));
    setValue((prev) => (isChanged(prev, computed) ? computed : prev));
  }

  useListener({
    field: key,
    stf,
    onInit: onUpdate,
    onUpdate: onUpdate,
    onDelete() {
      const computed = compute(undefined);
      setValue((prev) => (isChanged(prev, computed) ? computed : prev));
    },
  });

  return [value, (newValue: unknown) => engine.update(key, newValue)] as const;
}
```
Sources: [packages/stf/src/lib/stf.tsx:190-237](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L190-L237)

| Hook / Utility | Parameters | Return Value | Purpose |
| :--- | :--- | :--- | :--- |
| `useStf` | `options: { defaultValues?: DefaultValue<Record<string, unknown>> }` | `Stf` | Creates a memoized `Stf` context object managing a new `DataEngine`. |
| `useDataEngine` | `instance?: Stf \| DataEngine` | `DataEngine` | Resolves the data engine instance from arguments or context. |
| `useFieldValue` | `key: FieldKey, options?: { stf, defaultValue, compute, isChanged }` | `[value, (newValue) => void]` | Subscribes to a field path, computing values and returning a getter tuple with an updater. |
| `useListener` | `listener: DataEngineListener & { stf?: Stf \| DataEngine }` | `void` | Subscribes raw lifecycle listeners (`onInit`, `onUpdate`, `onDelete`) to a field key. |

Sources: [packages/stf/src/lib/stf.tsx:21-36](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L21-L36), [packages/stf/src/lib/stf.tsx:38-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L38-L42), [packages/stf/src/lib/stf.tsx:190-237](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L190-L237), [packages/stf/src/lib/stf.tsx:239-258](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L239-L258)

### Array and Object Collection Hooks

Collection controls are provided via `useArray` and `useObject`, mapping structured schema properties and array indices to interactive form bindings. `useArray` computes array item information objects and exposes methods to insert or remove items at specific indices.

```tsx
export function useArray(
  field: FieldKey,
  options: { defaultValue?: DefaultValue<unknown[]> } = {},
) {
  const engine = useDataEngine();
  const { defaultValue } = options;
  const [items] = useFieldValue(field, {
    defaultValue,
    compute(value) {
      const items: ArrayItemInfo[] = [];
      if (!Array.isArray(value)) return items;

      for (let i = 0; i < value.length; i++) {
        items.push({
          field: [...field, i],
          index: i,
        });
      }
      return items;
    },
    isChanged(prev, next) {
      return prev.length !== next.length;
    },
  });

  return {
    items,
    insertItem(itemValue?: unknown) {
      const value = engine.get(field);
      const idx = Array.isArray(value) ? value.length : 0;
      engine.init([...field, idx], itemValue);
    },
    removeItem(index: number) {
      engine.delete([...field, index]);
    },
  };
}
```
Sources: [packages/stf/src/lib/stf.tsx:49-85](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L49-L85)

`useObject` inspects plain objects using defined properties, pattern properties, fallbacks, and lazy evaluation flags, returning an array of property item descriptors alongside append and delete handlers.

```tsx
export function useObject<T>(
  field: FieldKey,
  options: {
    defaultValue?: DefaultValue<object>;
    /** ignore fixed properties unless defined  */
    lazy?: boolean;
    properties: Record<string, T>;
    patternProperties?: Record<string, T>;
    fallback?: T;
  },
) {
  const {
    properties: definedProps,
    patternProperties: definedPatternProps = {},
    defaultValue,
    fallback,
    lazy,
  } = options;
  const engine = useDataEngine();
  const [objectKeys] = useFieldValue(field, {
    defaultValue,
    compute(currentValue) {
      return isPlainObject(currentValue) ? Object.keys(currentValue) : [];
    },
    isChanged(prev, next) {
      return !deepEqual(prev, next);
    },
  });

  const properties = useMemo(() => {
    const properties: PropertyItemInfo<T>[] = [];
    const unknownKeys = new Set(objectKeys);
    for (const [key, prop] of Object.entries(definedProps)) {
      if (lazy && !unknownKeys.has(key)) continue;
      unknownKeys.delete(key);

      properties.push({
        kind: 'fixed',
        field: [...field, key],
        key,
        info: prop,
      });
    }

    for (const [pattern, prop] of Object.entries(definedPatternProps)) {
      const regex = RegExp(pattern);

      for (const key of unknownKeys) {
        if (!key.match(regex)) continue;
        unknownKeys.delete(key);
        properties.push({
          kind: 'pattern',
          info: prop,
          key,
          pattern,
          field: [...field, key],
        });
      }
    }

    if (fallback) {
      for (const key of unknownKeys) {
        properties.push({
          kind: 'fallback',
          field: [...field, key],
          key,
          info: fallback,
        });
      }
    }

    return properties;
  }, [definedPatternProps, definedProps, fallback, field, lazy, objectKeys]);

  return {
    properties,
    _objectKeys: objectKeys,
    onAppend(name: string, value?: unknown) {
      name = name.trim();
      if (name.length === 0) return;
```
Sources: [packages/stf/src/lib/stf.tsx:101-188](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L101-L188)

> [!WARNING]
> When using `useObject` with `lazy: true`, fixed properties defined in `properties` are ignored unless they are already present as keys in the underlying object data.

Sources: [packages/stf/src/lib/stf.tsx:101-135](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/stf/src/lib/stf.tsx#L101-L135)

## Markdown Ingestion and Dynamic Parsing

### Block and Inline Transforms

Fumadocs processes raw markdown and inline snippets through a unified pipeline combining `remark`, `remark-gfm`, `remark-rehype`, and `hast-util-to-jsx-runtime`. The `createMarkdownRenderer` function configures a processor that parses content into an mdast tree, runs transforms, and evaluates it into React elements via JSX runtimes.

Sources: [packages/core/src/content/md.ts:30-48](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/content/md.ts#L30-L48)

```typescript
const processor = remark()
  .use(remarkPlugins)
  .use(remarkRehype, remarkRehypeOptions)
  .use(rehypePlugins);
```
Sources: [packages/core/src/content/md.ts:35-38](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/content/md.ts#L35-L38)

Specialized markdown processors adapt block structures for interactive contexts. For instance, live previews apply a custom `rehypeWrapWords` visitor that splits text and element nodes into individual word spans decorated with animation classes, while skipping `pre` block nodes.

Sources: [packages/preview/src/components/markdown.tsx:24-54](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/markdown.tsx#L24-L54)

> [!WARNING]
> The `rehypeWrapWords` transformer explicitly skips `pre` elements (`node.tagName === 'pre'`) to prevent code block contents from being fractured into individual word spans.

Sources: [packages/preview/src/components/markdown.tsx:27](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/markdown.tsx#L27)

Code blocks embedded within markdown are intercepted by preprocessors like `MarkdownPre` or `Pre`, which extract language names from class attributes (converting `language-mdx` to `md`) and forward stripped code content to specialized rendering components.

Sources: [packages/preview/src/components/markdown.tsx:80-95](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/markdown.tsx#L80-L95), [packages/asyncapi/src/ui/index.tsx:222-235](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/ui/index.tsx#L222-L235)

### Twoslash Code Intelligence Integration

Code intelligence and type popup annotations are injected via `transformerTwoslash`, which wraps Shiki highlighters with a lazy-loaded Twoslasher instance configured with `moduleResolution: 100` (`ModuleResolutionKind.Bundler`).

Sources: [packages/twoslash/src/index.ts:31-52](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L31-L52)

```typescript
  return (cachedInstance ??= createTwoslasher({
    ...twoslashOptions,
    compilerOptions: {
      moduleResolution: 100 satisfies ModuleResolutionKind.Bundler,
      baseUrl: undefined,
      ...twoslashOptions.compilerOptions,
    },
  }));
}
```
Sources: [packages/twoslash/src/index.ts:38-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L38-L46)

The renderer configures Hast node mappings for hover triggers, popup contents, and documentation boxes, translating JSDoc links (`{@link ...}`) during nested markdown parsing.

Sources: [packages/twoslash/src/index.ts:60-107](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L60-L107), [packages/twoslash/src/index.ts:133-137](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L133-L137)

| Hast Element Mapping | Target Tag Name | Properties / Classes | Purpose |
| :--- | :--- | :--- | :--- |
| `hoverToken` | `Popup` | — | Wraps inspected tokens requiring hover inspection. |
| `hoverPopup` | `PopupContent` | `class: 'nd-copy-ignore'` | Container for type and documentation popups. |
| `popupDocs` | `div` | `class: 'prose twoslash-popup-docs'` | Styles JSDoc documentation sections. |
| `popupTypes` | `div` | `class: 'twoslash shiki fd-codeblock prose-no-margin'` | Renders type signature code blocks inside popups. |
| `nodesHighlight` | `span` | `class: 'highlighted-word twoslash-highlighted'` | Highlights code tokens associated with type info. |

Sources: [packages/twoslash/src/index.ts:61-105](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L61-L105)

## Related

- [Source Loader](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/source-loader)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/llms.txt) for all pages in this wiki.
