Skip to content

createUnique, again.#54

Merged
adebola-io merged 55 commits intomainfrom
unique-revamp
Mar 21, 2026
Merged

createUnique, again.#54
adebola-io merged 55 commits intomainfrom
unique-revamp

Conversation

@adebola-io
Copy link
Collaborator

Refactors createUnique to use a hook-based pattern and replaces createUniqueTransition with a composable UniqueTransition component.

Breaking Changes

createUnique

  • Removed options parameter (onSave, onRestore, container)
  • Added onMove() hook for state preservation
  • No longer renders <retend-unique-instance> wrapper
// Before
const Component = createUnique((props) => <div>{props.get().title}</div>, {
  onSave: (el) => ({ scroll: el.scrollTop }),
  onRestore: (el, data) => {},
});

// After
const Component = createUnique((props) => {
  onMove(() => {
    const scroll = ref.get()?.scrollTop;
    return () => {
      ref.get().scrollTop = scroll;
    };
  });
  return <div ref={ref}>{props.get().title}</div>;
});

createUniqueTransitionUniqueTransition

// Before
const Animated = createUniqueTransition(fn, { transitionDuration: '300ms' });

// After
const Animated = createUnique((props) => (
  <UniqueTransition transitionDuration="300ms">
    <div>{props.get().title}</div>
  </UniqueTransition>
));

Migration

  1. Replace onSave/onRestore with onMove() hook
  2. Replace createUniqueTransition(fn, opts) with createUnique(fn) + <UniqueTransition>
  3. Remove CSS targeting retend-unique-instance
  4. Update imports: import { onMove } from 'retend', import { UniqueTransition } from 'retend-utils/components'

The `saveContainerState` method was removed from the `CanvasRenderer`,
`TerminalRenderer`, and `VDOMRenderer` classes. This method was not
being used in any of these renderers.
Replaces `linkNodes` with `createGroupFromNodes` for creating a group
from rendered output within the `createUnique` function. This change
improves encapsulation and clarifies the internal rendering process.

Adds several new tests for the `unique` component, covering scenarios
such as:
- Components moving repeatedly without re-running `onSetup`.
- Components returning multiple elements, strings, or nothing.
- Components correctly updating plain props after moving.
This commit introduces a new experiment named `unique-test` to explore
unique component behavior with transitions.

The experiment includes:
- An `index.html` file for the basic page structure.
- A `package.json` file with dependencies for Retend and Vite.
- `App.tsx` which demonstrates `createUnique` with `UniqueTransition`
  for animating elements.
- `WithParentTransitions.tsx` showcases nested `UniqueTransition`
  components and dialog animations.
- `main.ts` and `router.ts` for application entry point and routing
  setup.
- `tsconfig.json` and `vite.config.ts` for project configuration.

Additionally, this commit refactors `retend-utils` to rename
`createUniqueTransition` to `UniqueTransition` for better clarity and
consistency. The internal implementation has also been updated to align
with the new component structure.
Introduce helper functions for save, restore, and dispose logic. Update
the component to utilize these functions, enhancing code organization
and readability. Add `activeHandle` and `isStable` properties to the
`UniqueCtx` for better state management.
The `#handleData` WeakMap was used to store data associated with
handles, but this data was never actually used and could be removed.
The `Unique` component's state management has been refactored to utilize
the renderer's `save` and `restore` methods. This allows for more
efficient and robust handling of component state when moving between
different render targets or during re-renders.

Key changes:
- Introduced `idOfLastSavedHandle` to `UniqueCtx` to track the
  identifier of the last saved handle.
- Modified `save` function to call `renderer.save` and store the
  returned ID.
- Modified `restore` function to call `renderer.restore` with the stored
  ID.
- Updated `onSetup` and the cleanup function within `createUnique` to
  correctly use the new save/restore mechanism.
- Removed obsolete `transfer` methods from `VDOMRenderer` and
  `DOMRenderer` as they are no longer needed.
- Removed `restoreContainerState` from `CanvasRenderer`,
  `TerminalRenderer`, and `dom-ops.js`.
- Added a new `MissingScopeError` to provide better context for
  scope-related issues.
Changed the internal representation of element rects from an array to a
Map, associating each element with its boundingClientRect. This
simplifies state management and ensures correct mapping between elements
and their dimensions during transitions.
Also removed unnecessary Symbol and related property accessors.
Reset `idOfLastSavedHandle` to null after restoring a saved handle. This
ensures that subsequent calls to `save()` correctly trigger the
`moveFns` after pending setup effects have been run.
Introduce `pendingNodes` to the unique instance to correctly defer DOM
writes when transitions are occurring. This ensures that DOM operations
are batched and applied in the correct order, preventing potential
rendering glitches during updates.
Store whether an animation was playing before being paused, so that it
can be
resumed correctly after the transition.
Adds a spinning animation to the Box component when it's connected to
the DOM. This uses the `onConnected` lifecycle hook and CSS animations.
The `createUniqueTransition` factory function has been renamed to
`UniqueTransition` and is now a component. This change aligns with the
naming convention of other utility components and provides a more
intuitive API.

The `UniqueTransition` component should be used within a `createUnique`
component to animate its children when the unique instance moves within
the DOM tree. The component supports FLIP animations and various
transition properties.
Introduces a new OXLint rule to disallow `Cell.derived()` calls directly
within JSX expressions. This promotes cleaner code by encouraging the
hoisting of derived cells to the component scope. The rule has been
added to the OXLint configurations for `docs`, `retend-start`, and
`retend-web-devtools`. The `Panel.tsx` component has been refactored to
use this new pattern.
The `props` cell was being created outside of the `withState` function.
This caused it to be destroyed when the parent context was destroyed,
leading to unexpected behavior.

By moving the creation of `props` inside `withState`, it now persists
within the unique state's context, ensuring it remains available as long
as the unique state is active.
The `journey` array now stores tuples of `[handle, UniqueProps<any>]`
instead of just `handle`. This allows for the restoration of previous
props when a unique instance is removed, ensuring the correct state is
maintained.
The plugin incorrectly flagged the usage of `Cell` within `Cell.derived`
and `Cell.derivedAsync` as an error. This commit adds checks to prevent
such false positives by examining the parent AST nodes.
This commit modifies the UniqueTransition component to correctly capture
and restore CSSTransition animations in addition to CSSAnimation.

The `saveState` function now collects CSSTransitions alongside
CSSAnimations. The `restoreTransition` function iterates through these
saved CSSTransitions and applies them to their respective targets,
ensuring animations that are not CSSAnimations are preserved.
This change refactors how child nodes are processed in `Block`
components.
Previously, `createGroupFromNodes` was used, which could lead to issues
with
unique component re-rendering across different routing layers.

The new implementation directly creates a group and links nodes,
ensuring
proper handling of unique components when navigating between routes and
outlets. This is demonstrated in new tests added to `matching.spec.tsx`
and
`unique.spec.tsx`.
Verify that unique components maintain synchronization when navigating
back and forth through nested route outlets.
Introduced a new `flattenNodes` utility function to handle the
flattening of arrays and groups of nodes returned from component
renders. This simplifies the logic within the `For` component and
ensures consistent handling of various node structures.
The whitespace in the router outlet rendering has been adjusted for
better readability.
Applies a priority of -1 to listeners in `For`, `If`, and `Switch`
components. This ensures that these components' listeners are processed
after other, potentially higher-priority, listeners, preventing
unexpected re-renders or race conditions.
This commit introduces a new experiment focused on testing the
'retend-utils' package. It sets up a new Vite project within the
'experiments' directory, including necessary configuration files, HTML,
CSS, and TypeScript entry points.

The experiment leverages various hooks and components from
'retend-utils' and 'retend-server' to demonstrate their functionality in
a real-world application context. This includes examples for:

- Element bounding and window size tracking.
- Media query matching.
- Live date display.
- Network status monitoring.
- Local and session storage integration.
- Router locking and navigation.
- Input component usage.
- Fluid list rendering.
- Cursor position tracking.
- Scope management.
- `onSetup`, `onConnected`, and `onMounted
Adds handling for `nodeType === 11` (DocumentFragment) within
`flattenJSXChildren`,
allowing hydration of components that return document fragments.
Includes a new
test case to verify this functionality.
This commit refactors the `getTopLevelJsxComponents` function to
properly handle exported functions and variables. It now correctly
identifies top-level components declared within `export default` and
`export named` statements, ensuring that components defined in these
common patterns are recognized.

The logic for identifying component candidates has been improved to
iterate through declarations within export statements and then check if
these declarations are indeed functions or variable declarations that
could represent components.

Additionally, a check for `BlockStatement` type has been added to ensure
that only functions with a body block are processed, preventing errors
with arrow functions not immediately returning JSX. This also implicitly
handles cases where a component might not have a function body.
@cloudflare-workers-and-pages
Copy link
Contributor

cloudflare-workers-and-pages bot commented Mar 20, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
retend e4b19e2 Commit Preview URL

Branch Preview URL
Mar 21 2026, 04:44 PM

@qodo-code-review
Copy link
Contributor

Review Summary by Qodo

Refactor createUnique to hook-based pattern with UniqueTransition component

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation

Grey Divider

Walkthroughs

Description
• **Refactored createUnique to use hook-based onMove() pattern** instead of onSave/onRestore
  options, enabling more flexible state preservation during component transitions
• **Replaced createUniqueTransition factory with composable UniqueTransition component** that
  wraps children and integrates with createUnique via the onMove() hook
• **Removed <retend-unique-instance> wrapper element** and replaced container state management
  with handle-based snapshot system using renderer.save() and renderer.restore()
• **Updated renderer interfaces** across DOM, VDOM, and CLI renderers to use numeric IDs for
  snapshot management instead of custom data objects
• **Fixed async component setup lifecycle** by moving unsuspend() and enable() calls inside
  onSetup() callback in Await component
• **Enhanced linting rules** with new noDerivedInJsx rule to prevent inline Cell.derived() in
  JSX, and improved component detection for exported declarations
• **Added comprehensive test coverage** for onMove() hook functionality, router-specific unique
  components, and hydration scenarios
• **Added utility testing experiments** with new utils-testing and unique-test workspaces
  demonstrating lifecycle hooks, scope context, storage hooks, and unique component animations
• **Optimized devtools performance** by hoisting derived cell creation outside of loops in
  PositionDropdown and Panel components
• **Added DocumentFragment support** to JSX flattening and improved serialized comment pair
  detection for ranges
• **Updated documentation** to reflect new onMove() hook pattern and UniqueTransition
  component-based approach
Diagram
flowchart LR
  A["createUnique<br/>with onMove hook"] -- "replaces" --> B["Old createUnique<br/>with onSave/onRestore"]
  C["UniqueTransition<br/>component"] -- "replaces" --> D["createUniqueTransition<br/>factory"]
  A -- "uses" --> E["renderer.save()<br/>renderer.restore()"]
  C -- "integrates via" --> A
  F["Enhanced Linting<br/>no-derived-in-jsx"] -- "prevents" --> G["Inline Cell.derived<br/>in JSX"]
  H["New Test Experiments<br/>utils-testing<br/>unique-test"] -- "demonstrates" --> A
Loading

Grey Divider

File Changes

1. packages/retend/source/library/unique.js ✨ Enhancement +229/-269

Refactor createUnique to hook-based pattern with onMove

• Refactored createUnique to use hook-based onMove() pattern instead of onSave/onRestore
 options
• Removed <retend-unique-instance> wrapper element rendering
• Replaced container state management with handle-based snapshot system using renderer.save() and
 renderer.restore()
• Introduced UniqueScope for managing move callbacks and state preservation during component
 transitions

packages/retend/source/library/unique.js


2. packages/retend/source/library/renderer.d.ts ✨ Enhancement +4/-7

Update renderer interface for snapshot-based state management

• Removed SavedNodeState type from RendererTypes interface
• Replaced saveContainerState() and restoreContainerState() methods with save(handle) and
 restore(id, handle) methods
• Updated method signatures to use numeric IDs for snapshot management instead of custom data
 objects

packages/retend/source/library/renderer.d.ts


3. packages/retend-utils/source/components/UniqueTransition.js ✨ Enhancement +357/-0

Add UniqueTransition component for animated unique moves

• New component that provides FLIP animation for unique component transitions
• Implements state saving/restoring with animation frame coordination
• Handles element positioning, scaling, and CSS animation state preservation
• Uses onMove() hook to integrate with createUnique lifecycle

packages/retend-utils/source/components/UniqueTransition.js


View more (86)
4. packages/retend-utils/source/components/index.ts ✨ Enhancement +1/-1

Update component exports for UniqueTransition

• Replaced export of createUniqueTransition with UniqueTransition

packages/retend-utils/source/components/index.ts


5. packages/retend-web/source/dom-renderer.js ✨ Enhancement +85/-21

Implement snapshot-based save/restore in DOM renderer

• Removed saveContainerState() and restoreContainerState() methods
• Added save(handle) method that captures nodes between handle markers and stores them with
 numeric IDs
• Added restore(id, handle) method that retrieves saved nodes and writes them to new handle
 location
• Added internal maps #savedHandles and #savedHandleId for snapshot storage
• Enhanced hydration range detection to handle serialized comment pairs

packages/retend-web/source/dom-renderer.js


6. packages/retend-server/source/v-dom/renderer.js ✨ Enhancement +29/-19

Implement snapshot-based save/restore in VDOM renderer

• Removed saveContainerState() and restoreContainerState() methods
• Added save(handle) method that captures nodes and stores with numeric IDs
• Added restore(id, handle) method that retrieves and writes saved nodes
• Added internal maps #savedHandles and #savedHandleId for snapshot storage

packages/retend-server/source/v-dom/renderer.js


7. experiments/retend-opentui/source/cli-renderer.ts ✨ Enhancement +3/-4

Update CLI renderer for new snapshot interface

• Removed SavedNodeState from OpenTuiRendererOptions
• Updated saveContainerState() to save() method stub
• Updated restoreContainerState() to restore() method stub

experiments/retend-opentui/source/cli-renderer.ts


8. packages/retend/source/library/scope.js ✨ Enhancement +9/-2

Add MissingScopeError for scope context errors

• Added MissingScopeError class for better error handling in scope context
• Updated useScopeContext() to throw MissingScopeError instead of generic Error

packages/retend/source/library/scope.js


9. packages/retend/source/library/for.js ✨ Enhancement +26/-2

Add node flattening utility for For component

• Added flattenNodes() utility function to properly handle groups and arrays in For loops
• Updated node tracking to use flattenNodes() for consistent handling of renderer groups

packages/retend/source/library/for.js


10. packages/retend/source/library/block.js ✨ Enhancement +3/-5

Simplify block component group creation

• Replaced createGroupFromNodes() with direct renderer.createGroup() and linkNodes() calls
• Simplified fragment rendering logic

packages/retend/source/library/block.js


11. packages/retend/source/library/utils.js ✨ Enhancement +1/-1

Update node template creation for groups

• Changed createNodesFromTemplate() to push groups directly instead of unwrapping them

packages/retend/source/library/utils.js


12. packages/retend/source/library/await.js 🐞 Bug fix +2/-2

Fix async component setup lifecycle ordering

• Moved unsuspend() and enable() calls inside onSetup() callback
• Ensures proper lifecycle ordering for async component setup

packages/retend/source/library/await.js


13. packages/retend/source/library/if.js ✨ Enhancement +2/-2

Improve type safety for If condition callbacks

• Updated JSDoc typedef for ConditionObject to use NonNullable<T> for true branch callback

packages/retend/source/library/if.js


14. packages/retend/source/router/router.js ✨ Enhancement +14/-14

Remove batching from router state updates

• Commented out Cell.batch() wrapper around route state updates
• Allows individual state updates to propagate immediately

packages/retend/source/router/router.js


15. packages/retend-web/source/dom-ops.js ✨ Enhancement +30/-32

Add serialized comment pair detection for ranges

• Added isSerializedCommentPair() function to detect serialized range markers
• Updated resolveRangeSegment() to recognize both matching and serialized comment pairs

packages/retend-web/source/dom-ops.js


16. packages/retend-web/source/plugins/hmr.js ✨ Enhancement +2/-2

Use Reflect API for HMR component invalidator

• Replaced direct property access with Reflect.get() and Reflect.set() for HMR invalidator

packages/retend-web/source/plugins/hmr.js


17. packages/retend-web/source/utils.js ✨ Enhancement +4/-0

Add DocumentFragment support to JSX flattening

• Added handling for DocumentFragment nodes (nodeType 11) in flattenJSXChildren()

packages/retend-web/source/utils.js


18. packages/retend-oxlint-plugin/index.js ✨ Enhancement +145/-48

Enhance linting rules and component detection

• Enhanced component detection to handle exported declarations
• Improved JSX detection logic with better candidate filtering
• Added noDerivedInJsx rule to prevent inline Cell.derived() in JSX
• Updated error messages for clarity and consistency
• Increased max component line limit from 150 to 100

packages/retend-oxlint-plugin/index.js


19. packages/retend-start/index.js ⚙️ Configuration changes +1/-0

Add no-derived-in-jsx to default linting rules

• Added retend/no-derived-in-jsx rule to default ESLint configuration

packages/retend-start/index.js


20. packages/retend-web-devtools/source/utils/sourceMapUtils.ts ✨ Enhancement +9/-3

Handle anonymous unique components in devtools

• Added check for anonymous unique components using __retendUnique marker
• Prevents caching of anonymous unique component names

packages/retend-web-devtools/source/utils/sourceMapUtils.ts


21. tests/unique.spec.tsx 🧪 Tests +573/-82

Update unique tests for onMove hook pattern

• Added comprehensive tests for onMove() hook functionality
• Added tests for multiple element returns, string returns, and empty returns
• Added race condition tests for Await and synchronous location transitions
• Updated existing tests to use onMove() instead of onSave/onRestore
• Removed assertions checking for retend-unique-instance wrapper element

tests/unique.spec.tsx


22. tests/router/unique.spec.tsx 🧪 Tests +275/-0

Add router-specific unique component tests

• New test suite for unique components in routing scenarios
• Tests unique component movement across routing layers and outlets
• Tests integration with UniqueTransition component

tests/router/unique.spec.tsx


23. docs/content/17-unique.mdx 📝 Documentation +31/-44

Update unique instances documentation

• Updated documentation to reflect onMove() hook pattern
• Removed references to onSave/onRestore options and container parameter
• Removed documentation about <retend-unique-instance> wrapper element
• Updated examples to use onMove() for state preservation

docs/content/17-unique.mdx


24. docs/content/27-utility-components.mdx 📝 Documentation +55/-56

Update UniqueTransition documentation

• Renamed createUniqueTransition to UniqueTransition component
• Updated documentation to show UniqueTransition as a child component of createUnique
• Updated examples and API documentation for new component-based approach
• Added examples using onMove() for state preservation

docs/content/27-utility-components.mdx


25. experiments/utils-testing/vite.config.ts ⚙️ Configuration changes +37/-0

Add utils-testing Vite configuration

• New Vite configuration for utils-testing experiment
• Configured SSG pages and router module path

experiments/utils-testing/vite.config.ts


26. experiments/utils-testing/source/router.ts ⚙️ Configuration changes +26/-0

Add utils-testing router configuration

• New router setup for utils-testing experiment
• Defines routes for various utility component tests

experiments/utils-testing/source/router.ts


27. experiments/utils-testing/source/views/start/routes.ts ⚙️ Configuration changes +78/-0

Add start view route definitions

• New route definitions for start view with child routes
• Includes routes for element-bounding, window-size, match-media, and other utilities

experiments/utils-testing/source/views/start/routes.ts


28. experiments/utils-testing/source/main.ts ⚙️ Configuration changes +8/-0

Add utils-testing main entry point

• New entry point for utils-testing experiment
• Initializes hydration with router

experiments/utils-testing/source/main.ts


29. experiments/utils-testing/index.html ⚙️ Configuration changes +13/-0

Add utils-testing HTML template

• New HTML template for utils-testing experiment

experiments/utils-testing/index.html


30. experiments/utils-testing/page.html ⚙️ Configuration changes +15/-0

Add utils-testing SSG page template

• New page template for utils-testing SSG

experiments/utils-testing/page.html


31. experiments/utils-testing/source/styles/base.css ⚙️ Configuration changes +20/-0

Add utils-testing base styles

• New base stylesheet for utils-testing experiment

experiments/utils-testing/source/styles/base.css


32. experiments/utils-testing/source/views/start/styles.module.css ⚙️ Configuration changes +9/-0

Add start view styles

• New module stylesheet for start view

experiments/utils-testing/source/views/start/styles.module.css


33. experiments/utils-testing/source/views/test-pages/page1.module.css ⚙️ Configuration changes +3/-0

Add page 1 styles

• New module stylesheet for page 1

experiments/utils-testing/source/views/test-pages/page1.module.css


34. experiments/utils-testing/source/views/test-pages/page2.module.css ⚙️ Configuration changes +3/-0

Add page 2 styles

• New module stylesheet for page 2

experiments/utils-testing/source/views/test-pages/page2.module.css


35. experiments/utils-testing/source/views/test-pages/shared.css ⚙️ Configuration changes +3/-0

Add test pages shared styles

• New shared stylesheet for test pages

experiments/utils-testing/source/views/test-pages/shared.css


36. experiments/unique-test/vite.config.ts ⚙️ Configuration changes +21/-0

Add unique-test Vite configuration

• New Vite configuration for unique-test experiment
• Configured SSG pages and router module path

experiments/unique-test/vite.config.ts


37. experiments/unique-test/source/router.ts ⚙️ Configuration changes +13/-0

Add unique-test router configuration

• New router setup for unique-test experiment
• Defines routes for unique component testing

experiments/unique-test/source/router.ts


38. experiments/unique-test/source/main.ts ⚙️ Configuration changes +9/-0

Add unique-test main entry point

• New entry point for unique-test experiment
• Initializes hydration with router

experiments/unique-test/source/main.ts


39. experiments/unique-test/index.html ⚙️ Configuration changes +12/-0

Add unique-test HTML template

• New HTML template for unique-test experiment

experiments/unique-test/index.html


40. tests/hydration/hydration-router-parent-dialog.spec.tsx 🧪 Tests +20/-17

Update hydration test for UniqueTransition

• Updated to use createUnique with UniqueTransition instead of createUniqueTransition
• Moved styling from container option to wrapper div

tests/hydration/hydration-router-parent-dialog.spec.tsx


41. pnpm-lock.yaml Dependencies +116/-41

Dependency updates and new experiment workspaces

• Added two new experiment workspaces (experiments/unique-test and experiments/utils-testing)
 with dependencies on retend packages
• Updated @types/node from 25.4.0 to 25.5.0 across multiple packages and their dependencies
• Updated vite and related plugin versions to use the new @types/node version
• Removed retend-web-devtools dependency from tests workspace

pnpm-lock.yaml


42. packages/retend-start/docs/.docs/packages/references/retend-utils.md 📝 Documentation +49/-49

Refactor createUniqueTransition to UniqueTransition component

• Renamed createUniqueTransition factory function to UniqueTransition component
• Changed from factory pattern with renderFn and options to a component that wraps children
• Removed onSave, onRestore, and container options; replaced with onMove hook usage
• Updated all code examples to show new createUnique + UniqueTransition pattern
• Added note that UniqueTransition must be rendered within a createUnique component

packages/retend-start/docs/.docs/packages/references/retend-utils.md


43. packages/retend-utils/README.md 📝 Documentation +37/-47

Update UniqueTransition documentation and examples

• Renamed createUniqueTransition to UniqueTransition in documentation
• Changed from factory function pattern to component-based pattern
• Removed onSave, onRestore, and container parameters
• Updated all examples to use createUnique wrapper with UniqueTransition child component
• Simplified documentation to reflect new composable approach

packages/retend-utils/README.md


44. experiments/utils-testing/source/views/start/use-setup-effect.tsx 🧪 Tests +155/-0

New lifecycle hooks testing component

• New test file demonstrating onSetup and onConnected lifecycle hooks
• Shows component mounting/unmounting behavior with console logging
• Includes list item management with add/remove functionality
• Tests effect cleanup and lifecycle ordering

experiments/utils-testing/source/views/start/use-setup-effect.tsx


45. tests/hydration/hydration-router-unique.spec.tsx 🧪 Tests +127/-13

Update hydration tests to new unique component pattern

• Updated Box component from createUniqueTransition factory to createUnique +
 UniqueTransition pattern
• Added new test case for stack-mode direct entry with unique client-only content
• Imports updated to use UniqueTransition instead of createUniqueTransition
• Added imports for ClientOnly, Outlet, defineRoutes, and useCurrentRoute

tests/hydration/hydration-router-unique.spec.tsx


46. experiments/unique-test/source/App.tsx 🧪 Tests +129/-0

New unique component test application

• New test application demonstrating createUnique with UniqueTransition
• Shows animated counter component that moves between 8 button containers
• Includes CSS animations (rotate, pulse, bloom) within the unique transition
• Uses onSetup hook for interval management

experiments/unique-test/source/App.tsx


47. experiments/unique-test/source/WithParentTransitions.tsx 🧪 Tests +95/-0

Unique transitions with parent animations test

• New component demonstrating createUnique with UniqueTransition in list and modal scenarios
• Shows items that animate between list view and modal dialog
• Uses onConnected hook for dialog animation and lifecycle management
• Demonstrates FLIP animation with custom Web Animations API

experiments/unique-test/source/WithParentTransitions.tsx


48. packages/retend-web-devtools/source/components/PositionDropdown.tsx ✨ Enhancement +29/-21

Optimize position dropdown with hoisted derived cells

• Extracted position state derivations (isTopLeft, isTopRight, etc.) to component scope
• Moved derived cells outside of the For loop to avoid recreating them per iteration
• Updated positions array to include isActive derived cell reference
• Simplified button rendering by using pre-computed derived cells

packages/retend-web-devtools/source/components/PositionDropdown.tsx


49. experiments/utils-testing/source/views/start/scope.tsx 🧪 Tests +81/-0

New scope context testing component

• New test file demonstrating scope context usage with createScope and useScopeContext
• Shows theme and user scope providers with nested components
• Includes derived cells for reactive styling based on theme
• Tests scope isolation and context consumption patterns

experiments/utils-testing/source/views/start/scope.tsx


50. experiments/utils-testing/source/views/start/index.tsx 🧪 Tests +73/-0

Utils testing navigation and routing page

• New router navigation page for utils testing experiments
• Links to various test pages (element bounding, window size, storage, etc.)
• Uses both new Link component and legacy router.Link patterns
• Demonstrates router outlet usage

experiments/utils-testing/source/views/start/index.tsx


51. experiments/utils-testing/source/views/start/input-test.tsx 🧪 Tests +60/-0

Input component testing page

• New test component for Input utility component
• Tests text, number, password, checkbox, and date input types
• Shows two-way binding with Cell models
• Demonstrates derived cells for reactive display formatting

experiments/utils-testing/source/views/start/input-test.tsx


52. experiments/utils-testing/source/views/start/fluid-list.tsx 🧪 Tests +66/-0

FluidList component testing page

• New test component for FluidList utility component
• Demonstrates list with add, remove, and shuffle operations
• Shows inline layout with configurable columns and gaps
• Tests dynamic item management and animations

experiments/utils-testing/source/views/start/fluid-list.tsx


53. tests/router/matching.spec.tsx 🧪 Tests +65/-1

Add unique component routing test

• Added new test case for moving unique components across routing layers and outlets
• Tests that unique component identity is preserved when moved between parent and grandchild routes
• Verifies unique content appears in correct location after navigation

tests/router/matching.spec.tsx


54. packages/retend-start/docs/.docs/packages/references/advanced-components.md 📝 Documentation +20/-11

Update createUnique documentation with onMove hook

• Updated createUnique documentation to reference UniqueTransition component instead of
 createUniqueTransition
• Replaced onSave/onRestore callbacks with onMove hook pattern
• Updated code example to show onMove hook usage for state preservation
• Clarified that onMove hook receives a callback that returns a cleanup/restore function

packages/retend-start/docs/.docs/packages/references/advanced-components.md


55. docs/content/20-await.mdx 📝 Documentation +2/-4

Clarify Await component documentation

• Removed note about Await being a JSX component vs function-based control flow
• Updated description of async cell detection to be more concise
• Changed reference from <If> to If() in comment for consistency

docs/content/20-await.mdx


56. experiments/utils-testing/source/views/start/session-storage.tsx 🧪 Tests +49/-0

Session storage hook testing page

• New test component for useSessionStorage hook
• Demonstrates session storage binding with input element
• Shows reactive updates between cell and storage
• Includes listener pattern for syncing input value

experiments/utils-testing/source/views/start/session-storage.tsx


57. tests/hydration/client-only.spec.tsx 🧪 Tests +39/-1

Add ClientOnly within unique hydration test

• Added new test case for hydrating ClientOnly within a unique subtree
• Tests that hydration works without range errors when unique components wrap client-only content
• Verifies no hydration errors occur in this scenario

tests/hydration/client-only.spec.tsx


58. experiments/utils-testing/source/views/start/local-storage.tsx 🧪 Tests +48/-0

Local storage hook testing page

• New test component for useLocalStorage hook
• Demonstrates local storage binding with input element
• Shows reactive updates between cell and storage
• Includes listener pattern for syncing input value

experiments/utils-testing/source/views/start/local-storage.tsx


59. packages/retend-web-devtools/source/components/Panel.tsx ✨ Enhancement +16/-12

Optimize panel position derivations

• Extracted position state derivations to component scope before JSX
• Created positionTopLeft, positionTopRight, positionBottomLeft, positionBottomRight derived
 cells
• Replaced inline Cell.derived() calls in class object with pre-computed derived cell references
• Improves performance by avoiding recreation of derived cells on each render

packages/retend-web-devtools/source/components/Panel.tsx


60. tests/hydration/hydration.spec.tsx 🧪 Tests +32/-0

Add document fragment hydration test

• Added new test case for hydrating document fragment children
• Tests that document fragments with text nodes hydrate without errors
• Verifies fragment content is properly preserved and accessible
• Imports getActiveRenderer for fragment creation

tests/hydration/hydration.spec.tsx


61. docs/.oxlintrc.json ⚙️ Configuration changes +17/-1

Add linting rules for derived cells in JSX

• Added retend/no-derived-in-jsx rule as error
• Added overrides section for specific files to disable max-component-lines and
 max-jsx-components-per-file rules
• Allows large components in EcosystemIllustrations, Footer, and DocsPage files

docs/.oxlintrc.json


62. experiments/utils-testing/source/views/start/router-lock.tsx 🧪 Tests +51/-0

Router lock functionality testing page

• New test component for router locking functionality
• Demonstrates router.lock() and router.unlock() methods
• Shows routelockprevented event handling
• Tests navigation prevention when router is locked

experiments/utils-testing/source/views/start/router-lock.tsx


63. experiments/utils-testing/source/views/start/element-bounding.tsx 🧪 Tests +39/-0

Element bounding hook testing page

• New test component for useElementBounding hook
• Demonstrates bounding rectangle tracking on resizable element
• Shows real-time updates of top, right, bottom, left, height, and width properties

experiments/utils-testing/source/views/start/element-bounding.tsx


64. experiments/utils-testing/source/views/start/match-media.tsx 🧪 Tests +29/-0

Match media hook testing page

• New test component for useMatchMedia hook
• Tests multiple media queries (dark mode, orientation, screen size, standalone mode)
• Shows reactive boolean values for each media query

experiments/utils-testing/source/views/start/match-media.tsx


65. packages/retend-web-devtools/source/components/ProviderChain.tsx ✨ Enhancement +7/-8

Add selected state derivation to provider chain

• Added selectedState derived cell to track if a provider in the chain is selected
• Derived cell checks if devRenderer.selectedNode matches any provider in the chain
• Improves performance by computing selection state once instead of inline

packages/retend-web-devtools/source/components/ProviderChain.tsx


66. experiments/utils-testing/source/views/start/network-status.tsx 🧪 Tests +32/-0

Network status hook testing page

• New test component for useOnlineStatus hook
• Demonstrates online/offline status detection
• Shows reactive UI updates based on network connectivity

experiments/utils-testing/source/views/start/network-status.tsx


67. experiments/utils-testing/tsconfig.json ⚙️ Configuration changes +25/-0

TypeScript configuration for utils testing

• New TypeScript configuration for utils-testing experiment
• Configured for ESNext target with JSX support
• Includes path aliases for @/* imports
• Strict mode enabled with proper DOM library types

experiments/utils-testing/tsconfig.json


68. experiments/unique-test/tsconfig.json ⚙️ Configuration changes +24/-0

TypeScript configuration for unique test

• New TypeScript configuration for unique-test experiment
• Configured for ESNext target with JSX support
• Includes path aliases for @/* imports
• Strict mode enabled with proper DOM library types

experiments/unique-test/tsconfig.json


69. docs/content/03-reactivity-and-cells.mdx 📝 Documentation +1/-0

Add derived cell hoisting best practice

• Added guidance to hoist Cell.derived() calls before JSX
• Recommends creating derived cells in component scope and passing them to JSX
• Prevents inline derived cell creation which can cause unnecessary recomputations

docs/content/03-reactivity-and-cells.mdx


70. experiments/utils-testing/source/views/start/live-date.tsx 🧪 Tests +25/-0

Live date hook testing page

• New test component for useLiveDate hook
• Demonstrates live date and time updates
• Shows locale time string formatting with derived cell

experiments/utils-testing/source/views/start/live-date.tsx


71. experiments/unique-test/package.json ⚙️ Configuration changes +23/-0

Package configuration for unique test

• New package.json for unique-test experiment workspace
• Includes dev server and build scripts
• Dependencies on all retend packages

experiments/unique-test/package.json


72. experiments/utils-testing/package.json ⚙️ Configuration changes +23/-0

Package configuration for utils testing

• New package.json for utils-testing experiment workspace
• Includes dev server and build scripts
• Dependencies on all retend packages

experiments/utils-testing/package.json


73. experiments/utils-testing/source/views/start/window-size.tsx 🧪 Tests +23/-0

Window size hook testing page

• New test component for useWindowSize hook
• Demonstrates window width and height tracking
• Shows reactive updates on window resize

experiments/utils-testing/source/views/start/window-size.tsx


74. docs/content/28-rendering-architecture.mdx 📝 Documentation +1/-1

Update For control flow reference

• Changed reference from <For> to For() for consistency with function-based control flow

docs/content/28-rendering-architecture.mdx


75. docs/source/components/ThemeToggle.tsx Formatting +1/-2

Reorder ThemeToggle component code

• Moved destructuring of class prop before derived cell creation
• Reordered code to hoist derived cells after prop destructuring
• Improves code organization and follows best practices

docs/source/components/ThemeToggle.tsx


76. packages/retend-start/docs/.docs/packages/_quick-reference.md 📝 Documentation +3/-5

Update quick reference imports

• Added createUnique and onMove to core imports
• Changed createUniqueTransition to UniqueTransition in utils imports
• Updated import statements to reflect new component API

packages/retend-start/docs/.docs/packages/_quick-reference.md


77. docs/source/routes/docs/DocsSidebar.tsx Formatting +1/-1

Reorder DocsSidebar derived cells

• Reordered code to hoist derived cell creation before function definition
• Moved isClosed derived cell before toggle function
• Improves code organization and follows best practices

docs/source/routes/docs/DocsSidebar.tsx


78. experiments/utils-testing/source/views/start/cursor-position.tsx 🧪 Tests +15/-0

Cursor position hook testing page

• New test component for useCursorPosition hook
• Demonstrates cursor X and Y position tracking
• Shows real-time cursor position updates

experiments/utils-testing/source/views/start/cursor-position.tsx


79. packages/retend-start/docs/.docs/packages/references/cells-api.md 📝 Documentation +1/-0

Add derived cell hoisting guidance

• Added guidance to hoist Cell.derived() out of JSX
• Recommends passing derived cell into JSX instead of creating it inline
• Helps prevent unnecessary recomputations

packages/retend-start/docs/.docs/packages/references/cells-api.md


80. experiments/utils-testing/.vscode/extensions.json ⚙️ Configuration changes +9/-0

VS Code extensions configuration

• New VS Code extensions configuration for utils-testing experiment
• Recommends Biome, Prettier, TypeScript, CSS modules, and CSS sorting extensions

experiments/utils-testing/.vscode/extensions.json


81. packages/retend-web-devtools/.oxlintrc.json ⚙️ Configuration changes +7/-0

Add oxlint configuration for devtools

• New oxlint configuration for retend-web-devtools package
• Extends base configuration and includes retend-oxlint-plugin
• Enables retend/no-derived-in-jsx rule as error

packages/retend-web-devtools/.oxlintrc.json


82. experiments/utils-testing 📦 Other +0/-0

experiments/utils-testing


83. experiments/cross-renderer-demo/source/renderers/canvas/renderer.ts Additional files +0/-6

...

experiments/cross-renderer-demo/source/renderers/canvas/renderer.ts


84. experiments/cross-renderer-demo/source/renderers/terminal/renderer.ts Additional files +0/-6

...

experiments/cross-renderer-demo/source/renderers/terminal/renderer.ts


85. experiments/utils-testing/source/views/start/llm-test.tsx Additional files +3/-0

...

experiments/utils-testing/source/views/start/llm-test.tsx


86. experiments/utils-testing/source/views/test-pages/page1.tsx Additional files +6/-0

...

experiments/utils-testing/source/views/test-pages/page1.tsx


87. experiments/utils-testing/source/views/test-pages/page2.tsx Additional files +6/-0

...

experiments/utils-testing/source/views/test-pages/page2.tsx


88. packages/retend-utils/source/components/createUniqueTransition.js Additional files +0/-261

...

packages/retend-utils/source/components/createUniqueTransition.js


89. packages/retend-web-devtools/source/core/devtools-renderer.ts Additional files +0/-1

...

packages/retend-web-devtools/source/core/devtools-renderer.ts


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Mar 20, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. Reused consumed save id🐞 Bug ✓ Correctness
Description
createUnique calls renderer.restore(instance.idOfLastSavedHandle, handle) without clearing
idOfLastSavedHandle, but restore() consumes the saved snapshot id; a subsequent move before
onSetup resets the id will reuse a non-existent snapshot id and the move becomes a no-op (nodes
won’t move). This can break unique-instance relocation/animations under rapid successive renders
(e.g., multiple moves before activation).
Code

packages/retend/source/library/unique.js[R238-255]

+        if (length !== instance.journey.length || !instance.isStable) {
+          // Next instance, when last instance is not yet stable.
+          // Move the nodes, but do not run move effects.
+          const previousHandle =
+            instance.journey[instance.journey.length - 1][0];
+          instance.journey.push([handle, nextProps]);
+          instance.idOfLastSavedHandle = renderer.save(previousHandle);
+          renderer.restore(instance.idOfLastSavedHandle, handle);
+        } else {
+          // Next instance, when last instance is stable.
+          // Move and run effects.
+          // If last instance was already disposed, it would have already
+          // run the moveFn effects, so all we need to do is restore. If it is an
+          // active instance however, we need to run both save() and restore()
+          // on the fly.
+          instance.idOfLastSavedHandle = save(instance, renderer);
+          instance.journey.push([handle, nextProps]);
+          renderer.restore(instance.idOfLastSavedHandle, handle);
Evidence
In createUnique move logic, idOfLastSavedHandle is set and immediately restored, but never
cleared there; later save() short-circuits if idOfLastSavedHandle is non-null. However,
DOMRenderer.restore() deletes the saved snapshot entry, so reusing the same id means restore will
find no nodes and return early, leaving the unique subtree at the previous location.

packages/retend/source/library/unique.js[107-123]
packages/retend/source/library/unique.js[231-266]
packages/retend/source/library/unique.js[269-272]
packages/retend-web/source/dom-renderer.js[156-180]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`createUnique` stores `instance.idOfLastSavedHandle` and calls `renderer.restore(id, handle)`, but `DOMRenderer.restore()` consumes (deletes) that id. If another move happens before `onSetup` runs, `save()` may return the stale id and the next `restore()` becomes a no-op, preventing nodes from moving.
### Issue Context
- `save()` returns `inst.idOfLastSavedHandle` when non-null.
- `renderer.restore()` deletes the saved snapshot entry.
- `onSetup` resets `idOfLastSavedHandle` later, so there’s a window where a second move can reuse an already-consumed id.
### Fix Focus Areas
- packages/retend/source/library/unique.js[231-266]
- packages/retend-web/source/dom-renderer.js[156-180]
### Implementation notes
After each successful `renderer.restore(id, handle)` call inside the `move()` logic (both stable and non-stable branches), set `instance.idOfLastSavedHandle = null` (or otherwise track pending-restore state separately) so subsequent `save()` calls don’t reuse an already-consumed id.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. restore() disallows null🐞 Bug ✓ Correctness
Description
The Renderer type defines restore(id, handle: Handle) but core createUnique calls
renderer.restore(id, null) during teardown, making the published TS types inconsistent with actual
runtime behavior. This will force TS consumers/renderer implementers into type assertions or fail
compilation under strict typing.
Code

packages/retend/source/library/renderer.d.ts[R128-131]

+  /** Saves the current state of a handle to the renderer's snapshot store. */
+  save(handle: Handle): number;
+  /** Restores a handle's state from the renderer's snapshot store. */
+  restore(id: number, handle: Handle): void;
Evidence
createUnique passes null to renderer.restore on journey end, while the Renderer interface
declares handle as non-nullable. The DOM renderer implementation already supports null handles
by deleting the saved entry and only writing when handle is truthy.

packages/retend/source/library/renderer.d.ts[125-132]
packages/retend/source/library/unique.js[291-299]
packages/retend-web/source/dom-renderer.js[171-180]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `Renderer.restore` TypeScript signature requires a non-null handle, but core code calls `restore(id, null)` to flush/forget a saved snapshot. This is a type/API mismatch.
### Issue Context
- `createUnique` uses `renderer.restore(savedId, null)` during teardown.
- Renderer implementations already tolerate `null` handles.
### Fix Focus Areas
- packages/retend/source/library/renderer.d.ts[125-132]
- packages/retend/source/library/unique.js[291-299]
### Implementation notes
Change the interface to `restore(id: number, handle: Handle | null): void;` (and keep `save(handle: Handle): number`). Ensure any related docs/comments reflect that `null` is a valid handle value for flushing a saved snapshot.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. TerminalRenderer lacks save/restore🐞 Bug ⚙ Maintainability
Description
TerminalRenderer still declares implements Renderer but the PR removed the old
saveContainerState/restoreContainerState stubs and did not add the newly required save/restore
methods. This should fail TypeScript compilation for the cross-renderer demo package.
Code

experiments/cross-renderer-demo/source/renderers/terminal/renderer.ts[L659-664]

-  saveContainerState(): null {
-    return null;
-  }
-
-  restoreContainerState(): void {}
-
Evidence
The Renderer interface now requires save(handle) and restore(id, handle). TerminalRenderer
implements Renderer but its class ends without these methods, and the diff shows the previous
placeholder methods were removed.

packages/retend/source/library/renderer.d.ts[125-132]
experiments/cross-renderer-demo/source/renderers/terminal/renderer.ts[474-483]
experiments/cross-renderer-demo/source/renderers/terminal/renderer.ts[646-666]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`TerminalRenderer` implements the `Renderer` interface, but after the interface change it no longer defines the required `save(handle)` and `restore(id, handle)` methods.
### Issue Context
The PR removed `saveContainerState/restoreContainerState` stubs, but the interface now expects handle-based `save/restore`.
### Fix Focus Areas
- experiments/cross-renderer-demo/source/renderers/terminal/renderer.ts[646-666]
- packages/retend/source/library/renderer.d.ts[125-132]
### Implementation notes
Add `save(handle: TerminalHandle): number` and `restore(id: number, handle: TerminalHandle | null): void` methods. If the terminal renderer can’t support this, implement minimal stubs (e.g., throw `new Error(&amp;amp;amp;amp;#x27;Not implemented&amp;amp;amp;amp;#x27;)`) to satisfy the interface and keep the experiment compiling.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47a2606b1e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@adebola-io
Copy link
Collaborator Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47a2606b1e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Adds a cleanup function to the `onMove` hook to remove the callback from
the set when the component unmounts. This prevents memory leaks and
ensures correct behavior when components are conditionally rendered.
Wrap route updates in a `Cell.batch` call to ensure they are processed
atomically. This prevents potential race conditions and ensures UI
consistency.
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 21, 2026

Open in StackBlitz

retend

npm i https://pkg.pr.new/resuite/retend@54

retend-oxlint-plugin

npm i https://pkg.pr.new/resuite/retend/retend-oxlint-plugin@54

retend-server

npm i https://pkg.pr.new/resuite/retend/retend-server@54

retend-start

npm i https://pkg.pr.new/resuite/retend/retend-start@54

retend-utils

npm i https://pkg.pr.new/resuite/retend/retend-utils@54

retend-web

npm i https://pkg.pr.new/resuite/retend/retend-web@54

retend-web-devtools

npm i https://pkg.pr.new/resuite/retend/retend-web-devtools@54

commit: e4b19e2

The `idOfLastSavedHandle` variable was not being reset to `null` after
being used to restore a renderer state. This could lead to incorrect
state restoration in subsequent operations. This commit ensures that the
variable is reset after each usage to prevent potential bugs.
Adds a check to ensure `onMove` callback in `UniqueTransition` only runs
when the renderer is a `DOMRenderer`. This prevents unnecessary
operations when working with non-DOM renderers.

Additionally, this commit refactors the `createUnique` function to
ensure `instance.state.node.disable()` and
`instance.idOfLastSavedHandle` are only set when the current handle is
the last one in the journey. This prevents premature state disabling
when an older handle is being removed.
Implements placeholder methods for saving and restoring terminal states.
These methods currently return default values and do nothing,
as their full implementation is not yet required.
@adebola-io adebola-io merged commit e45ee6a into main Mar 21, 2026
4 checks passed
@adebola-io adebola-io deleted the unique-revamp branch March 21, 2026 16:48
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