Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion packages/react/src/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
import { describe, it, expect } from "vitest";
import { flatToTree } from "./hooks";
import { renderHook, act } from "@testing-library/react";
import { flatToTree, useUITree } from "./hooks";
import type { UITree } from "@json-render/core";

describe("useUITree", () => {
it("starts with empty tree by default", () => {
const { result } = renderHook(() => useUITree());
expect(result.current.tree).toEqual({ root: "", elements: {} });
});

it("applies patches", () => {
const { result } = renderHook(() => useUITree());
act(() => {
result.current.applyPatch({ op: "set", path: "/root", value: "main" });
result.current.applyPatch({
op: "add",
path: "/elements/main",
value: { key: "main", type: "Stack", props: {} },
});
});
expect(result.current.tree.root).toBe("main");
expect(result.current.tree.elements["main"]).toBeDefined();
});

it("clears tree", () => {
const initialTree: UITree = {
root: "main",
elements: { main: { key: "main", type: "Stack", props: {} } },
};
const { result } = renderHook(() => useUITree({ initialTree }));
act(() => {
result.current.clear();
});
expect(result.current.tree).toEqual({ root: "", elements: {} });
});
});

describe("flatToTree", () => {
it("converts array of elements to tree structure", () => {
Expand Down
41 changes: 41 additions & 0 deletions packages/react/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,47 @@ function applyPatch(tree: UITree, patch: JsonPatch): UITree {
return newTree;
}

/**
* Options for useUITree
*/
export interface UseUITreeOptions {
/** Initial tree state */
initialTree?: UITree;
}

/**
* Return type for useUITree
*/
export interface UseUITreeReturn {
/** Current UI tree */
tree: UITree;
/** Apply a patch to the tree */
applyPatch: (patch: JsonPatch) => void;
/** Replace the entire tree */
setTree: (tree: UITree) => void;
/** Reset to empty tree */
clear: () => void;
}

/**
* Hook for managing a UI tree from external patch sources.
*/
export function useUITree(options?: UseUITreeOptions): UseUITreeReturn {
const [tree, setTree] = useState<UITree>(
options?.initialTree ?? { root: "", elements: {} },
);

const applyPatchFn = useCallback((patch: JsonPatch) => {
setTree((current) => applyPatch(current, patch));
}, []);

const clear = useCallback(() => {
setTree({ root: "", elements: {} });
}, []);

return { tree, applyPatch: applyPatchFn, setTree, clear };
}

/**
* Options for useUIStream
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ export {
// Hooks
export {
useUIStream,
useUITree,
flatToTree,
type UseUIStreamOptions,
type UseUIStreamReturn,
type UseUITreeOptions,
type UseUITreeReturn,
} from "./hooks";