Skip to content

Writing it well

The examples below come from one boss health bar.

If you would ever read a value back off the screen, keep it in a cell and draw the prop from it:

src/idiomatic.ts2
export const bossHudPinned = sessionState(0);
src/idiomatic.ts2
<sprite x={{ right: 8 }} y={5} w={16} h={16} sprite={Marks.PINNED}
hide={pinned === 0} transparency={fade} />

Decide from hp, not from bar.width.

A value that follows from other values is arithmetic where it is used:

src/idiomatic.ts2
<rect x={1} y={1} w={(TRACK_W - 2) * hp / Math.max(1, max)} h={{ fill: 2 }}
color={hp * 4 < max ? Colors.RED : 0x2fa84f} fill transparency={fade} />

When the value comes from a function that reads many server values, follow just its result with useDerived:

src/idiomatic.ts2
const threat = useDerived(() => threatOf(which));

A handler changes state, and the markup draws from it:

src/idiomatic.ts2
<layer w="fill" h="fill" rebuildOn={fight} onClick={() => { setPinned(1 - pinned); }}>
onClick={() => { lock.hide = false; }} // undone when the region is rebuilt

A component that was handed a ref, like a scrollbar given its pane, writes through it. See Refs.

Read a value in the component that uses it, not in a parent that passes it down:

src/idiomatic.ts2
<Window title="Boss">
<layer x={10} y={42} w={{ fill: 20 }} h={{ fill: 52 }}>
<BossHud />
</layer>
</Window>

One cell holds how far the animation has got, one interval advances it, and the render draws from it.

src/idiomatic.ts2
const [fade, setFade] = useState(0);
const host = useRef();
useInterval(() => { setFade(Math.min(255, fade + 16)); }, { everyMs: 20 }, host);

A timer on a hidden host does not run, so the host’s hide is the idle condition:

src/idiomatic.ts2
<layer ref={host} w={1} h={1} hide={hp > 0 || fade >= 255} />

When the animation has to line up with something, store when it started and compute the frame from elapsed time, not from a count.

Nothing under a hidden component updates, so hide the elements that draw and leave a component that reads state or runs a timer shown. When something should not exist at all, use && or return null rather than hide.

A function the server calls takes values, writes the cells they change, and names no component.

src/idiomatic.ts2
export const bossHudPin = (pinned: Int): void => {
const [, setPinned] = useState(bossHudPinned);
setPinned(pinned);
};
  • Components are PascalCase and named for what they draw: HealthBar.
  • Cells and functions are camelCase. A cell is named for the fact (bossHudPinned), not the handler that writes it (pinClicked).
  • Entry points are prefixed with the panel: bossHudPin.
  • Helpers are named for their answer: threatOf(which).
  • Constants are UPPER_CASE.

A game id with no generated name is named after what the game calls it:

src/refs-idiomatic.ts2
export const FEUD_KEBABMAN_POSTQUEST = 11876;

A large panel splits by kind: <panel>.state.ts2 for the cells, <panel>.component.ts2 for the components, <panel>.helpers.ts2 for the entry points and arithmetic.