1 count your renders
GOAL
predict the exact number of times each component function runs after a mount and after a parent setState, then check the guess against a redraw tally.
CONCEPT
a render is one call of your component function by React. React re-runs a component when its own state changes, and it re-runs every child of a re-rendering parent, even a child whose props did not change at all.
HINT
count function calls, not DOM updates. The two skills below take no props whatsoever, yet a click that only moves the parent's own state still reaches both of them.
MIRRORS
the skill tree panel on the HUD. Spending a single point redraws the panel, and every branch skill and leaf skill hanging under it redraws with it, including the ones nothing touched. That is why a real skill tree wraps its nodes in memo().
Run
This koan renders React, so it needs a one-time setup.
pnpm koan 08-render-detection/01-count-your-renders.tsxSource
// DOJO · Module 8 / Exercise 1 — count your renders
// GOAL: predict the exact number of times each component function runs
// after a mount and after a parent setState, then check the guess
// against a redraw tally.
// CONCEPT: a render is one call of your component function by React.
// React re-runs a component when its own state changes, and it
// re-runs every child of a re-rendering parent, even a child
// whose props did not change at all.
// HINT: count function calls, not DOM updates. The two skills below
// take no props whatsoever, yet a click that only moves the
// parent's own state still reaches both of them.
// MIRRORS: the skill tree panel on the HUD. Spending a single point
// redraws the panel, and every branch skill and leaf skill
// hanging under it redraws with it, including the ones nothing
// touched.
// That is why a real skill tree wraps its nodes in memo().
// Run: pnpm koan 08-render-detection/01-count-your-renders.tsx
import { useState } from 'react';
import { afterEach, expect, it } from 'vitest';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
// Redraw tally: each component bumps its own name as its body starts.
const redraws = new Map<string, number>();
function bump(name: string): void {
redraws.set(name, (redraws.get(name) ?? 0) + 1);
}
function count(name: string): number {
return redraws.get(name) ?? 0;
}
afterEach(() => {
cleanup();
redraws.clear();
});
function BranchSkill() {
bump('BranchSkill');
return <div>branch skill</div>;
}
function LeafSkill() {
bump('LeafSkill');
return <div>leaf skill</div>;
}
function SkillTree() {
bump('SkillTree');
const [spent, setSpent] = useState(0);
return (
<div>
<button onClick={() => setSpent(spent + 1)}>
{`points spent: ${spent}`}
</button>
<BranchSkill />
<LeafSkill />
</div>
);
}
type Tally = {
SkillTree: number | null;
BranchSkill: number | null;
LeafSkill: number | null;
};
function tally(): Tally {
return {
SkillTree: count('SkillTree'),
BranchSkill: count('BranchSkill'),
LeafSkill: count('LeafSkill'),
};
}
// --- TODO 1 ---------------------------------------------------------
// The first render of a component is its mount. render(<SkillTree />)
// mounts SkillTree, and SkillTree's JSX mounts BranchSkill and
// LeafSkill. Replace each null with the number of times that component
// function ran.
const AFTER_MOUNT: Tally = {
SkillTree: null,
BranchSkill: null,
LeafSkill: null,
};
// --- TODO 2 ---------------------------------------------------------
// The button calls setSpent, so only SkillTree's own state changes.
// BranchSkill and LeafSkill take no props, so nothing about them is
// different - apply the rule from the header anyway. Predict each total
// tally after the button has been clicked TWICE (the mount render is
// included in the totals).
const AFTER_TWO_CLICKS: Tally = {
SkillTree: null,
BranchSkill: null,
LeafSkill: null,
};
it('TODO 1 — a mount runs every component function once', () => {
render(<SkillTree />);
expect(tally(), 'TODO 1 — tally after the mount').toEqual(AFTER_MOUNT);
});
it('TODO 2 — a parent setState re-runs the children too', () => {
render(<SkillTree />);
const button = screen.getByRole('button');
fireEvent.click(button);
fireEvent.click(button);
expect(tally(), 'TODO 2 — tally after two clicks').toEqual(
AFTER_TWO_CLICKS,
);
});