XState
XState models list updates as actor events. The official @xstate/react useSelector() hook renders
the actor snapshot, while Likftc handles transition-safe keys.
Live before-and-after
Section titled “Live before-and-after”Executed source
Section titled “Executed source”Imported verbatim with ?raw from every implementation file shown below. Playback timing and FLIP controls come from the shared demo harness.
/** @jsxImportSource react */
import { useLikftc } from "@vp-tw/likftc/react";
import { useSelector } from "@xstate/react";
import { StrictMode } from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import { createActor, fromTransition, type ActorRefFrom } from "xstate";
import { mountFrameworkDemo, type DemoFrameState, type DemoRuntime } from "../shared/demo.js";
import { ReactStatePanels } from "../shared/react-state-panels.js";
type FrameEvent = { readonly frame: DemoFrameState; readonly type: "frame.changed" };
function Runtime({ actor }: { readonly actor: ActorRefFrom<ReturnType<typeof createFrameLogic>> }) {
const frame = useSelector(actor, (snapshot) => snapshot.context);
const entries = useLikftc(frame.items, { getId: (item) => item.id });
return <ReactStatePanels entries={entries} frame={frame} label="XSTATE" />;
}
function createFrameLogic(initialState: DemoFrameState) {
return fromTransition((_state: DemoFrameState, event: FrameEvent) => event.frame, initialState);
}
await mountFrameworkDemo("XState + React", (target, initialState): DemoRuntime => {
const actor = createActor(createFrameLogic(initialState));
actor.start();
const root = createRoot(target);
flushSync(() =>
root.render(
<StrictMode>
<Runtime actor={actor} />
</StrictMode>,
),
);
return {
destroy: () => {
flushSync(() => root.unmount());
actor.stop();
},
update: (frame) => flushSync(() => actor.send({ frame, type: "frame.changed" })),
};
});
/** @jsxImportSource react */
import {
createAfterRows,
createBeforeRows,
describeRow,
type DemoFrameState,
type DemoItem,
type DemoRenderedRow,
} from "./demo.js";
function Row({ row }: { readonly row: DemoRenderedRow }) {
return (
<li
className="runtime-row"
data-collision={String(row.kind === "collision")}
data-id={row.item.id}
data-key={row.keyText}
data-kind={row.kind}
data-phase={row.kind === "exiting" ? "exiting" : "current"}
data-slot={String(row.slot)}
>
<b>{row.item.id.toUpperCase()}</b>
<span>{row.item.label}</span>
<code>{row.keyText}</code>
<em>{describeRow(row.kind)}</em>
</li>
);
}
function Panel({
corrected,
label,
rows,
}: {
readonly corrected: boolean;
readonly label: string;
readonly rows: readonly DemoRenderedRow[];
}) {
return (
<article className="runtime-panel">
<small>{corrected ? `WITH LIKFTC + ${label}` : `WITHOUT + ${label}`}</small>
<h3>{corrected ? "Presence identity" : "Logical ID key"}</h3>
<ol data-list={corrected ? "after" : "before"}>
{rows.map((row) => (
<Row key={row.key} row={row} />
))}
</ol>
</article>
);
}
export function ReactStatePanels({
entries,
frame,
label,
}: {
readonly entries: readonly { readonly item: DemoItem; readonly key: number }[];
readonly frame: DemoFrameState;
readonly label: string;
}) {
return (
<div className="runtime-grid">
<Panel corrected={false} label={label} rows={createBeforeRows(frame)} />
<Panel corrected label={label} rows={createAfterRows(frame, entries)} />
</div>
);
}
This uses full XState v5 actor semantics rather than @xstate/store, so the example can grow into
guards, event routing, or actor composition without changing its React integration.