State & Hooks
Per-row state, memo, and effects — the React-shaped hooks rows use.
A row authored by render is a real React render path, so you reach for hooks —
but mugen's hooks, not React's, because their state must be readable without
mounting (the walker measures every row off-screen) and a change to it must
re-measure the row. They mirror useState / useMemo / useEffect.
Call them directly inside the
renderfunction (or a plain function it calls), in the same order every render — the usual rules of hooks. They throw in an event handler or outside a list; in a nested component (a JSX element the row composes), useuseMugenRowinstead.
useMugenState
State that can change a row's height — an expanded flag, an edited string, loaded
content. Shaped exactly like useState, but the value lives in the list instance
and a set re-measures only that row (O(log n)), even off-screen.
import {
, , , ,
, ,
} from '@wingleeio/mugen';
interface Thread {
: string;
: string;
: string;
}
const = ('button');
function (: Thread) {
const [, ] = (false);
return (
< ={12} ={() => (() => !)}>
< ={4}>
< ="600 15px Inter">{.}</>
{ ? <>{.}</> : null}
</>
</>
);
}
export function ({ }: { : Thread[] }) {
const = ({ : });
return (
< ={} ={() => .} ={}
="15px Inter" ={22} />
);
}There are no reducers or actions — a set is the whole API, just like React.
useMugenMemo
Derive a value per row, recomputed only when its deps change. Like useMemo,
but readable in the measure walk — use it to keep expensive per-row derivation out
of the hot path (the walker re-runs a row's body on every re-measure).
import { , , } from '@wingleeio/mugen';
declare function (: string): string[];
interface Doc {
: string;
: string;
}
export function (: Doc) {
const = (() => (.), [.]);
return (
< ={8} ={12}>
{.((, ) => (
< ={}>{}</>
))}
</>
);
}useMugenEffect
Run a side effect per row when deps change — to transform content (parse
markdown, highlight, load) and then set height-affecting state. It runs for
every row, on- or off-screen, so the height becomes exact the moment the work
resolves. See Effects for the full pattern.
useMugenRow — hooks in nested components
The positional hooks above only work at the root of render: their identity is
call order, and a nested component's call order can't be matched between the
measure walk and the React render. useMugenRow(id) is the form that can go
anywhere in the row's tree — it names a scope, and its methods mirror the
root hooks one-to-one:
import { , , } from '@wingleeio/mugen';
// A nested component the row composes — not the render root.
export function ({ , }: { : string; : string }) {
const = (`reply:${}`);
const [, ] = .(false);
return (
< ={() => (() => !)}>
< ="600 13px Inter">Reply</>
{ ? <>{}</> : null}
</>
);
}row.state / row.memo / row.effect behave exactly like their root
counterparts (state lives in the list instance, a set re-measures just that
row, effects run for every row on- or off-screen); row.tween is the scoped
useMugenTween. Two rules:
idmust be unique within the row — two instances of the same component need different ids, so derive one from a prop.- Call the scope's methods unconditionally, in a fixed order — they're positional within the scope, like any hooks.
There's no re-render tax hiding in this: the scope resolves through a context
whose value is one frozen object per row that never changes identity, so the
context itself never re-renders anything. Each consumer subscribes to its row's
version instead — which also means it stays fresh even when an ancestor element
is memo-stable (useMugenMemo) and React bails out of the subtree, and the
measure walk stays fresh the same way (a scoped write busts the walker's height
memo for that row).
Item updates are height triggers too
When you pass new items to useMugenVirtualizer, mugen re-keys and re-measures.
A row whose text now wraps to a different height re-anchors the scrollbar without
a flash — through the same path as a set.