diff --git a/packages/react/src/hooks.test.ts b/packages/react/src/hooks.test.ts index c86b92e..33e98d3 100644 --- a/packages/react/src/hooks.test.ts +++ b/packages/react/src/hooks.test.ts @@ -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", () => { diff --git a/packages/react/src/hooks.ts b/packages/react/src/hooks.ts index deb71f2..5b3c5f5 100644 --- a/packages/react/src/hooks.ts +++ b/packages/react/src/hooks.ts @@ -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( + 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 */ diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 8fd5b76..9dc66fd 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -51,7 +51,10 @@ export { // Hooks export { useUIStream, + useUITree, flatToTree, type UseUIStreamOptions, type UseUIStreamReturn, + type UseUITreeOptions, + type UseUITreeReturn, } from "./hooks";