- Fixed stale closure bug in streaming content by using useRef to properly accumulate content instead of relying on state closure - Added auto-scrolling functionality to keep latest content visible during generation - Implemented smooth scrolling behavior and improved HTML styling for better readability - Added content buffer reset when starting new generation - Enhanced documentation with detailed explanation of the closure problem and solution
4.1 KiB
Streaming Content Accumulation Fix
Problem
The streaming content was showing individual tokens instead of the accumulated HTML article because of a stale closure issue.
What Was Happening
// ❌ WRONG - Stale closure
onContent: (data) => {
onSetStreamingContent(streamingContent + data.delta);
// streamingContent is captured from the initial render
// It never updates in this callback!
}
Result: Each delta was added to the INITIAL empty string, not the accumulated content.
Delta 1: "" + "TypeScript" = "TypeScript"
Delta 2: "" + " is" = " is" // Lost "TypeScript"!
Delta 3: "" + " a" = " a" // Lost everything!
Solution
Use a ref to track the accumulated content outside the closure:
// ✅ CORRECT - Ref accumulation
const contentBufferRef = useRef<string>('');
onContent: (data) => {
contentBufferRef.current += data.delta;
onSetStreamingContent(contentBufferRef.current);
}
Result: Each delta is properly accumulated.
Delta 1: "" + "TypeScript" = "TypeScript"
Delta 2: "TypeScript" + " is" = "TypeScript is"
Delta 3: "TypeScript is" + " a" = "TypeScript is a"
Additional Improvements
1. Auto-Scroll
const streamingBoxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isGenerating && streamingContent && streamingBoxRef.current) {
streamingBoxRef.current.scrollTop = streamingBoxRef.current.scrollHeight;
}
}, [streamingContent, isGenerating]);
Automatically scrolls to show new content as it arrives.
2. Smooth Scrolling
sx={{
scrollBehavior: 'smooth',
// ...
}}
Smooth animation when auto-scrolling.
3. Better HTML Styling
'& h2, & h3': { mt: 2, mb: 1 },
'& p': { mb: 1 },
'& ul, & ol': { pl: 3, mb: 1 },
'& li': { mb: 0.5 },
Proper spacing for HTML elements.
Technical Explanation
Closure Problem
In JavaScript, when you create a callback function, it "closes over" the variables from its surrounding scope. Those variables are captured at the time the function is created.
const [content, setContent] = useState('');
// This callback is created once when component mounts
const callback = () => {
console.log(content); // Always logs the INITIAL value!
};
Why Refs Work
Refs are mutable objects that persist across renders:
const ref = useRef('');
// This always reads the CURRENT value
const callback = () => {
console.log(ref.current); // Gets the latest value!
};
Before vs After
Before (Broken)
User sees:
"TypeScript"
" is"
" a"
"powerful"
...individual tokens, no accumulation
After (Fixed)
User sees:
"TypeScript"
"TypeScript is"
"TypeScript is a"
"TypeScript is a powerful"
...proper accumulation, rendered as HTML
Files Changed
✅ apps/admin/src/components/steps/StepGenerate.tsx
- Added contentBufferRef for accumulation
- Added streamingBoxRef for auto-scroll
- Added useEffect for auto-scroll
- Fixed onContent callback to use ref
- Added smooth scroll behavior
Testing
- Start generation
- Watch the "Live Generation" box
- Should see:
- ✅ Proper HTML rendering (headings, paragraphs, lists)
- ✅ Content accumulating (not individual tokens)
- ✅ Auto-scroll to bottom
- ✅ Smooth animations
- ✅ Pulsing blue border
Common React Pitfalls
This is a classic React pitfall that catches many developers:
❌ Don't Do This
const [value, setValue] = useState(0);
setInterval(() => {
setValue(value + 1); // Always adds to initial value!
}, 1000);
✅ Do This Instead
const [value, setValue] = useState(0);
setInterval(() => {
setValue(prev => prev + 1); // Functional update
}, 1000);
// OR use a ref
const valueRef = useRef(0);
setInterval(() => {
valueRef.current += 1;
setValue(valueRef.current);
}, 1000);
Conclusion
The fix ensures that streaming content properly accumulates and displays as formatted HTML, providing a smooth, professional user experience with auto-scrolling and proper rendering.
Status: ✅ Fixed and working!