Skip to content

feat(interact): let a plugin refresh when its output is replaced - #386

Closed
ameerf-wix wants to merge 4 commits into
masterfrom
plugin-refresh
Closed

ameerf-wix wants to merge 4 commits into
masterfrom
plugin-refresh

Conversation

@ameerf-wix

@ameerf-wix ameerf-wix commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Interact runs the plugins on connect, and right after that resolves the effects' targets: the plugin's output, such as @wix/splittext's .split-c spans. If something replaces that output later, nothing runs the plugin again:

  • The new element isn't split.
  • The effects keep targeting the detached spans, so the animation runs on nodes that are no longer on the page.

React 18 hydration can cause this. When a component is hydrated only once it enters the viewport, the plugin has already split the server-rendered DOM. React finds that the DOM doesn't match and renders the component again, without the split. On a server-rendered page with three split-text titles, all three ended up un-split once they hydrated, and their sequences had no visible targets.

SplitTextResult.split() and the autoSplit re-split can't help here. Both re-split result.element, which is the element that was replaced.

Change

Who acts: the plugin notices, and Interact does the refresh.

  • Only the plugin knows which elements its output depends on. The plugin contract keeps Interact from looking inside what a plugin does, so Interact doesn't watch for this itself.
  • Only Interact can re-resolve the targets and re-bind the triggers. A plugin that split the new element again without Interact would leave the effects on the old spans.

@wix/interact: the plugin context gets refresh(). It reconnects the controller the plugin was applied to (update()), which:

  • runs the plugin cleanups,
  • re-applies the plugins,
  • re-resolves the targets.

It runs in a microtask, so a plugin can call it while it runs. Calls before it run once, and it does nothing once the plugin's cleanup has run (a disconnect replaces _appliedPlugins, which refresh checks).

@wix/splittext: when a container it split is no longer in the document, splitTextPlugin calls refresh().

  • A container leaves the document only when it or one of its ancestors is removed from its parent. So one MutationObserver watches childList on each node from the containers' parents up to the root, without subtree: typically a few nodes.
  • Its own split, including autoSplit re-splits, only changes nodes inside the containers, so it doesn't reach the observer. Neither do other changes in the root, unless they add or remove a direct child of one of those ancestors.
  • refresh is optional in the plugin's structural context type. With an Interact version that doesn't pass it, the plugin splits as before.

Docs: the plugins guide, InteractPluginContext in the types reference, rules/plugins.md, the interactor skill's plugin reference, the splittext README, and the CHANGELOG (@wix/interact 2.5.26, @wix/splittext 0.2.2).

Verification

  • Unit tests: 510 pass in interact and 149 in splittext, and tsc passes for both packages and the demo (whose splitText.ts checks the plugin is still assignable to InteractPlugin). Without the source change, the new tests that check refresh behaviour fail:
    • refresh() re-applies the plugin and the trigger binds to the new element.
    • Calls made while the plugin runs collapse into one reconnect.
    • refresh() does nothing after the cleanup, or after a reconnect that happened in between.
    • The plugin calls refresh when a re-render replaces its container or an ancestor between it and the root, but not for its own split, not for other changes in the root, and not after its cleanup.
    • The plugin observes only the containers' ancestors up to the root, without subtree.
    • With the real Interact, a replaced container is split again and marked data-splittext-ready.
  • Browser (Playwright Chromium): a production server-rendered page with three split-text titles, with the published @wix/interact and @wix/splittext builds swapped for this branch's.
    • Before: all three titles are un-split once they hydrate, and nothing animates.
    • After: they're split again, and their sequences run on the new spans: a flicker on 21 characters, and a letter-rise on 22 and on 54.
    • A second page has a split heading (slot-spin on characters) and a split paragraph (line-rise on lines) at the top. Before, both lose their split when they hydrate. After, the heading has 12 characters and the paragraph 2 lines, and both animations run.

Not covered

If a re-render replaces a container's contents but keeps the container element, the split is lost and the plugin doesn't notice. Only replacing the container itself, which is what hydration does here, triggers a refresh.

🤖 Generated with Claude Code

ameerf-wix and others added 2 commits October 30, 2026 15:24
Interact resolves an effect's targets once, right after the plugins run on
connect. If something later replaces a plugin's output, e.g. a framework
hydration that doesn't match the split DOM and renders the element again,
the effects keep targeting the detached elements and nothing re-runs the
plugin.

The plugin context gets `refresh()`, which reconnects the controller: it
runs the cleanups, re-applies the plugins and re-resolves the targets. It
runs in a microtask, once for all the calls before it, and does nothing
once the plugin's cleanup has run. The plugin decides when to call it,
since only it knows which elements its output depends on.

`splitTextPlugin` watches its root and calls `refresh()` when a container
it split is replaced. Its own split keeps the containers, so it doesn't
trigger it. With an Interact that passes no `refresh`, it splits as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Generated llms.txt preview

llms.txt
# @wix/interact

> Declarative, configuration-driven interaction library — web-native, AI-ready, and framework-agnostic.

- Install: `npm install @wix/interact @wix/motion-presets`
- Three entry points: vanilla JS (`@wix/interact`), React (`@wix/interact/react`), Web Components (`@wix/interact/web`)
- Five trigger types: hover, click, viewEnter, viewProgress, pointerMove
- Effects via named presets (`@wix/motion-presets`), keyframes, CSS transitions, or custom JS callbacks
- Configs are JSON-serializable -- designed for LLM generation

## Docs

- [Full Reference](https://wix.github.io/interact/rules/full-lean.md): Declarative configuration-driven interaction library. Binds animations to triggers via JSON config. (788 lines)
- [Integration Guide](https://wix.github.io/interact/rules/integration.md): Rules for integrating `@wix/interact` into a webpage — binding animations and effects to user-driven triggers via declarative configuration. (368 lines)
- [Validation Guide](https://wix.github.io/interact/rules/validate.md): Rules for using `@wix/interact-validate` — validate an `InteractConfig` before it reaches the runtime. (315 lines)

## Optional

- [Click Trigger Rules for @wix/interact](https://wix.github.io/interact/rules/click.md): This document contains rules for generating click-triggered interactions in `@wix/interact`. (197 lines)
- [Hover Trigger Rules for @wix/interact](https://wix.github.io/interact/rules/hover.md): This document contains rules for generating hover-triggered interactions in `@wix/interact`. (199 lines)
- [PointerMove Trigger Rules for @wix/interact](https://wix.github.io/interact/rules/pointermove.md): These rules help generate pointer-driven interactions using `@wix/interact`. (271 lines)
- [ViewEnter Trigger Rules for @wix/interact](https://wix.github.io/interact/rules/viewenter.md): This document contains rules for generating interactions that respond to elements entering the viewport using the `@wix/interact`. (232 lines)
- [ViewProgress Trigger Rules for @wix/interact](https://wix.github.io/interact/rules/viewprogress.md): These rules help generate scroll-driven interactions using `@wix/interact`. (233 lines)
- [@wix/interact Plugin Rules](https://wix.github.io/interact/rules/plugins.md): Rules for extending `@wix/interact` with external plugins via `Interact.use()` and `$`-prefixed config fields. (123 lines)
- [All rules in one file](https://wix.github.io/interact/llms-full.txt): Complete concatenation (2726 lines)
llms-full.txt header
# @wix/interact v2.5.25 -- AI Rules Reference
# https://wix.github.io/interact/llms.txt
# 9 files, 2726 lines

Download full files from workflow artifacts, or run node scripts/generate-llms.mjs locally.

ameerf-wix and others added 2 commits October 30, 2026 15:57
The plugin observed its whole root (`childList` + `subtree`), so the
browser queued a record for every node added or removed anywhere in it,
including the plugin's own re-splits and unrelated content.

A container leaves the document only when it or one of its ancestors is
removed from its parent. Observe `childList` on each node from the
containers' parents up to the root, without `subtree`: the split and other
changes inside the root no longer reach the observer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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