Hi everyone. Recently, I had to implement a feature where items in a large list needed to update at very short intervals (as fast as 10 milliseconds).
At first, I took the straightforward route: keep an index in state, iterate over the list, and update items one by one. But this approach wasn’t ideal:
- Too many re-renders because the interval was so short.
- Extra overhead from constantly saving and retrieving the index state.
So, I tried a different approach: using a generator to control updates while each item manages its own visibility state.
Imagine we want to hide characters in a large string, one by one, with precise control over start, stop, and resume actions.
We’ll create a Char component that manages its own isVisible state and exposes its setter to the parent.
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
export default function Char({
char,
index,
oninitial,
}: {
char: string;
index: number;
oninitial: (x: Dispatch<SetStateAction<boolean>>) => void;
}) {
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
oninitial(setIsVisible);
}, []);
return <span>{isVisible ? char : ""}</span>;
}
The Parent Component
The parent (CharsList) stores all setters in a ref and uses a generator to walk through them at a controlled pace.
import { useRef, type Dispatch, type SetStateAction } from "react";
import Char from "./Char";
function CharsList({ largeString }: { largeString: string }) {
const iterationRef = useRef<
Generator<Dispatch<SetStateAction<boolean>>, void, unknown> | null
>(null);
const intervalRef = useRef<number | null>(null);
const setters = useRef<Record<number, Dispatch<SetStateAction<boolean>>>>({});
// Generator to yield each setter
function* startChanger(arrayOfSetters: Dispatch<SetStateAction<boolean>>[]) {
for (const setter of arrayOfSetters) yield setter;
}
const startHiding = () => {
const iteration = startChanger(Object.values(setters.current));
iterationRef.current = iteration;
intervalRef.current = setInterval(() => {
const result = iteration.next();
if (result.done) {
iterationRef.current = null;
return clearInterval(intervalRef.current as number);
}
result.value(false);
}, 10);
};
const stopHiding = () => clearInterval(intervalRef.current as number);
const continueHiding = () => {
const iteration = iterationRef.current!;
intervalRef.current = setInterval(() => {
const result = iteration.next();
if (result.done) return clearInterval(intervalRef.current as number);
result.value(false);
}, 10);
};
return (
<>
<div className="card">
<button onClick={startHiding}>Start Hiding</button>
<button onClick={stopHiding}>Stop Hiding</button>
<button onClick={continueHiding}>Continue Hiding</button>
</div>
<p className="large-string-text">
{largeString.split("").map((char, index) => (
<Char
key={index}
char={char}
index={index}
oninitial={(setter) => {
setters.current[index] = setter;
}}
/>
))}
</p>
</>
);
}
export default CharsList;
This approach gives you:
- Better performance — only one component updates at a time.
- Pause & resume control — thanks to the generator’s statefulness.
but Higher complexity — not as straightforward as the index-based solution.
If you’re working with large lists and ultra-short intervals, the performance tradeoff is worth it.
This was a fun problem to solve. At first, the generator approach felt unintuitive, but it became a powerful pattern for precise, performant updates.
Thanks for reading. Would love to hear your thoughts or alternative solutions in the comments!
Top comments (1)
Smart use of generators. Offloading visibility control to individual components avoids unnecessary re-renders and keeps the update loop lean. Tradeoff in complexity feels justified at this scale