5 state lives at an address
GOAL
call in advance which of three HUD buttons wipes a skill's rank and which one leaves it standing, using the address rule alone.
CONCEPT
a hook's value is not filed under the component. React files it under an address of three parts - where the element sits in the tree, the key written on it, and which function is rendered there. Leave all three alone and the very same instance is reused. Disturb any one of them and the old instance is unmounted, a fresh one mounts, and useState starts over.
HINT
not one line of the skill's own body changes between the three clicks. Ask of each button which part of the address it edits.
MIRRORS
the skill tree on the player's HUD, where each leaf is keyed by the skill it holds and never by the slot it sits in. Retrain a slot into a different skill and that skill starts at rank zero, instead of inheriting the rank of whatever stood there.
Run
This koan renders React, so it needs a one-time setup.
pnpm koan 07-recursive-components/05-state-lives-at-an-address.tsxSource
// DOJO · Module 7 / Exercise 5 — state lives at an address
// GOAL: call in advance which of three HUD buttons wipes a skill's rank
// and which one leaves it standing, using the address rule alone.
// CONCEPT: a hook's value is not filed under the component. React files
// it under an address of three parts - where the element sits in
// the tree, the key written on it, and which function is rendered
// there. Leave all three alone and the very same instance is
// reused. Disturb any one of them and the old instance is
// unmounted, a fresh one mounts, and useState starts over.
// HINT: not one line of the skill's own body changes between the three
// clicks. Ask of each button which part of the address it edits.
// MIRRORS: the skill tree on the player's HUD, where each leaf is keyed
// by the skill it holds and never by the slot it sits in. Retrain
// a slot into a different skill and that skill starts at rank
// zero, instead of inheriting the rank of whatever stood there.
// Run: pnpm koan 07-recursive-components/05-state-lives-at-an-address.tsx
import { useState } from "react";
import { afterEach, describe, expect, it } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
afterEach(cleanup);
// A single leaf of the skill tree. How far it has been trained is its
// own local state, and nothing below ever touches it again.
function SkillNode() {
const [rank, setRank] = useState(0);
return (
<div>
<span data-testid="rank">{rank}</span>
<button onClick={() => setRank((r) => r + 1)}>train</button>
</div>
);
}
// Letter for letter the same body, declared as a SECOND function. Two
// functions that read alike are still two types as far as React cares.
function EliteSkillNode() {
const [rank, setRank] = useState(0);
return (
<div>
<span data-testid="rank">{rank}</span>
<button onClick={() => setRank((r) => r + 1)}>train</button>
</div>
);
}
// The branch that draws one leaf, under three buttons. Each button is
// rigged to disturb exactly one part of that leaf's address - and the
// third button disturbs none of them.
function SkillTree() {
const [slot, setSlot] = useState("blade");
const [elite, setElite] = useState(false);
const [, setRedraws] = useState(0);
const LeafSkill = elite ? EliteSkillNode : SkillNode;
return (
<div>
<button onClick={() => setRedraws((n) => n + 1)}>redraw hud</button>
<button
onClick={() => setSlot((s) => (s === "blade" ? "shield" : "blade"))}
>
change slot key
</button>
<button onClick={() => setElite((e) => !e)}>promote to elite</button>
<LeafSkill key={slot} />
</div>
);
}
// --- TODO 1 ----------------------------------------------------------
// Train the leaf twice, so the HUD reads 2, then press "redraw hud".
// That button only bumps a counter the branch itself holds, so the leaf
// is drawn again at the same spot, under the same key, from the same
// function. Write down the rank the HUD reads after the redraw.
const RANK_AFTER_HUD_REDRAW: number | null = null;
// --- TODO 2 ----------------------------------------------------------
// Train twice again, then press "change slot key". Same spot in the
// tree, same function - but the key on that element flips from "blade"
// to "shield". Write down the rank the HUD reads now.
const RANK_AFTER_SLOT_KEY_CHANGE: number | null = null;
// --- TODO 3 ----------------------------------------------------------
// Train twice again, then press "promote to elite". Same spot, same key
// - but SkillNode gives way to EliteSkillNode, the twin declared above.
// Write down the rank the HUD reads now.
const RANK_AFTER_TYPE_PROMOTION: number | null = null;
describe("Module 7 / Exercise 5 — state lives at an address", () => {
it("TODO 1 - same position, same key, same type", () => {
render(<SkillTree />);
fireEvent.click(screen.getByText("train"));
fireEvent.click(screen.getByText("train"));
fireEvent.click(screen.getByText("redraw hud"));
expect(
screen.getByTestId("rank").textContent,
"TODO 1: a redraw that leaves all three parts alone keeps the rank",
).toBe(String(RANK_AFTER_HUD_REDRAW));
});
it("TODO 2 - same position, NEW key", () => {
render(<SkillTree />);
fireEvent.click(screen.getByText("train"));
fireEvent.click(screen.getByText("train"));
fireEvent.click(screen.getByText("change slot key"));
expect(
screen.getByTestId("rank").textContent,
"TODO 2: a new key at the same spot is a different address",
).toBe(String(RANK_AFTER_SLOT_KEY_CHANGE));
});
it("TODO 3 - same position, same key, NEW type", () => {
render(<SkillTree />);
fireEvent.click(screen.getByText("train"));
fireEvent.click(screen.getByText("train"));
fireEvent.click(screen.getByText("promote to elite"));
expect(
screen.getByTestId("rank").textContent,
"TODO 3: a new component type at the same spot is a different address",
).toBe(String(RANK_AFTER_TYPE_PROMOTION));
});
});