mugen

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 render function (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), use useMugenRow instead.

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.

faq.tsx
loading preview…

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:

  • id must 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.

On this page