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 (0)