Welcome back! In the previous chapter, Theme & Glyph Utilities, we created a system to render beautiful characters and colors for our spinner.
However, we left off with a static character. A spinner that doesn't spin is just a typo. To make it come alive, we need it to update 20 times a second.
In React, when a component updates, it re-renders. If that component has a lot of children (like our Tree from Chapter 1), re-rendering the whole thing 20 times a second is like trying to run a sprint while wearing a backpack full of rocks. It consumes too much CPU, and the terminal will flicker or lag.
The Solution: We split the UI into two parts:
We call this The Isolated Animation Loop.
useAnimationFrame)
This is a special hook in our terminal library (ink) that forces a specific component to redraw itself repeatedly, regardless of what the rest of the app is doing.
Usually, in React, you pass data via props. When props change, the component re-renders.
But we don't want the Parent to re-render just to tell the Child that 50 milliseconds have passed.
Instead, we use Refs. Think of a Ref as a mailbox.
To use this pattern, the parent component prepares the "mailboxes" (Refs) and passes them to the high-performance child component (SpinnerAnimationRow).
The parent (SpinnerWithVerb) stays stable. It creates references for changing data, like how many tokens have been generated.
// Inside the Parent Component
// 1. Create a "mailbox" for the number of tokens
const responseLengthRef = useRef(0);
// 2. Update the mailbox whenever new data comes in
// Note: This does NOT trigger a re-render by itself!
responseLengthRef.current = task.tokenCount;
We pass these refs to our isolated component.
<SpinnerAnimationRow
// Pass the ref, not the value!
responseLengthRef={responseLengthRef}
// Pass static props (things that rarely change)
message="generating code..."
mode="thinking"
/>
Let's visualize how the two parts of the application run at different speeds.
The magic happens inside SpinnerAnimationRow.tsx. Let's look at how it uses the timer to drive the visuals.
We use useAnimationFrame to create a loop that runs every 50ms (20 frames per second).
// Inside SpinnerAnimationRow.tsx
export function SpinnerAnimationRow({ reducedMotion, ...props }) {
// This hook forces THIS component to re-render every 50ms
// It provides 'time' (milliseconds since start)
const [ref, time] = useAnimationFrame(reducedMotion ? null : 50);
// Calculate which frame of the spinner animation to show
const frame = Math.floor(time / 120);
Beginner Tip: If
reducedMotionis true (for accessibility), we passnull. This stops the loop entirely, saving CPU and preventing motion sickness.
We need to show a timer (e.g., 1.2s). We don't ask the parent "what time is it?" because that requires communication. We calculate it ourselves using the current time and a start time ref.
// Determine elapsed time without asking Parent
const now = Date.now();
const startTime = props.loadingStartTimeRef.current;
// Math: Current Time - Start Time - Total Paused Time
const elapsed = now - startTime - props.totalPausedMsRef.current;
const timerText = formatDuration(elapsed); // e.g., "1.2s"
The AI sends data in chunks. It might jump from 10 tokens to 50 tokens instantly. We want the number to "count up" smoothly (10, 11, 12... 50).
Because we are in a fast loop, we can increase the number by a small amount every frame until we catch up.
// "current" is where the AI is. "displayed" is what we show.
const actualCount = props.responseLengthRef.current;
const displayedCount = tokenCounterRef.current;
// If we are behind, catch up!
if (displayedCount < actualCount) {
// Add a small amount this frame
tokenCounterRef.current += 3;
}
Finally, we render the lightweight components using these calculated values. Notice we use SpinnerGlyph from the previous chapter!
return (
<Box ref={ref}>
{/* The spinning character */}
<SpinnerGlyph frame={frame} />
{/* The timer we calculated */}
<Text>{timerText}</Text>
{/* The smooth number we calculated */}
<Text>{displayedCount} tokens</Text>
</Box>
);
By isolating the Animation Loop, we have achieved high performance. The heavy "Thinking Tree" logic stays quiet while the "Spinner" runs wild. This keeps our CLI responsive and snappy.
We have a spinner, a timer, and a token counter. But the text itselfβthe message "thinking..."βlooks a bit static next to all this activity.
In the next chapter, we will use our animation loop to create a beautiful "shimmer" effect across the text.
Next Chapter: Text Glimmer Effects
Generated by Code IQ