# Common Streaming Component Pitfalls URL: /concepts/streaming/streaming-pitfalls Props stream incrementally during generation. This causes common runtime errors and infinite loops that are easy to fix once you know the patterns. ## 1. Component Not Re-rendering When Props Update The most common mistake is not handling prop updates during streaming. ### Using useState with props State initialized from props only runs once - it won't update as props stream in: ```tsx // ❌ BAD: State initialized once, never updates when props stream in function MyComponent({ title, content }: Props) { const [state, setState] = useState({ title, content }); // title and content update during streaming, but state doesn't! return
{state.title}
; // Shows stale/empty value } ``` ### Passing props directly to JSX Even without state, you need to handle undefined props during streaming: ```tsx // ❌ BAD: Props may be undefined or partial during streaming function MyComponent({ title, items }: Props) { return (

{title}

{/* May show nothing or partial text */} {items.map((item) => ( ))}{" "} {/* Crashes if items undefined */}
); } ``` **Solutions:** ```tsx // ✅ Option 1: Use useTamboComponentState with setFromProp for editable content function MyComponent({ title, content }: Props) { const [state, setState] = useTamboComponentState( "myState", { title: "", content: "" }, title && content ? { title, content } : undefined, ); return
{state.title}
; // Updates as props stream in } // ✅ Option 2: Use propStatus to wait for complete values // Best when you need complete data before rendering (e.g., forms, IDs, required fields) // Note: This hides content until complete - use Option 3 for progressive display function MyComponent({ title, items }: Props) { const { propStatus } = useTamboStreamStatus(); // Re-renders occur as propStatus changes during streaming return (
{propStatus.title?.isSuccess &&

{title}

} {propStatus.items?.isSuccess && items.map((item) => )}
); } // ✅ Option 3: Handle undefined with defaults (progressive display) // Best for showing values as they stream in character-by-character // Works because: thread state updates → parent re-renders → React reconciles // your component with new props (same key/position = update, not remount) function MyComponent({ title, items }: Props) { return (

{title ?? ""}

{(items ?? []).map((item, i) => ( ))}
); } ``` ## 2. Infinite useEffect Loops When merging streamed props with existing state, it's easy to create infinite loops: ```tsx // ❌ BAD: Infinite loop - state changes trigger effect, effect changes state function MultiSelect({ options }: Props) { const [state, setState] = useTamboComponentState("selections", { options: [], selected: [], }); useEffect(() => { // This runs when options change, but also when state changes! setState((prev) => ({ ...prev, options: options ?? [], })); }, [options, state, setState]); // state in deps = infinite loop } ``` ### Solution 1: Derive During Render (Recommended) Don't store derived data in state. Calculate it during render: ```tsx // ✅ No useEffect needed - derive merged state during render function MultiSelect({ options }: Props) { const { streamStatus } = useTamboStreamStatus(); // Store ONLY what the user controls const [selected, setSelected] = useTamboComponentState( "selected", [], ); // Derive the full state during render const currentOptions = options ?? []; return (
{currentOptions.map((opt, i) => ( ))}
); } ``` **Why this works:** Props (AI-generated) and state (user selections) are kept separate. No syncing needed because you're not duplicating the props into state. ### Solution 2: useRef Callback Pattern When you must merge props into state, use a ref to avoid stale closures: ```tsx // ✅ Works in React 18/19.1 - ref always has latest callback function useLatestCallback unknown>( callback: T, ): T { const ref = useRef(callback); useLayoutEffect(() => { ref.current = callback; }); return useCallback((...args: Parameters) => ref.current(...args), []) as T; } function MultiSelect({ options }: Props) { const [state, setState] = useTamboComponentState("state", { options: [], selected: [], }); const mergeOptions = useLatestCallback((incoming: string[]) => { // Always sees latest `state` without being a dependency return { ...state, options: incoming }; }); useEffect(() => { if (options !== undefined) { setState(mergeOptions(options)); } }, [options, mergeOptions, setState]); // All deps listed, no infinite loop } ``` ### Solution 3: useEffectEvent (React 19.2+) If you're on React 19.2+, use the official solution: ```tsx // ✅ Cleanest solution - requires React 19.2+ import { useEffectEvent } from "react"; function MultiSelect({ options }: Props) { const [state, setState] = useTamboComponentState("state", { options: [], selected: [], }); // useEffectEvent captures latest state without being reactive const onOptionsChange = useEffectEvent((incoming: string[]) => { setState({ ...state, options: incoming }); }); useEffect(() => { if (options !== undefined) { onOptionsChange(options); } }, [options]); // Only reactive values in deps } ``` **Solution 1 (Derive during render)** is best when you can restructure to keep props and state separate. This is the React-recommended approach. **Solution 2 (useRef callback)** works in all React versions when you must merge. **Solution 3 (useEffectEvent)** is cleanest but requires React 19.2+. ## 3. Missing Keys in Streamed Arrays When rendering arrays that stream in, items may not have IDs yet: ```tsx // ❌ ERROR: "Each child in a list should have a unique key prop" { matches.map((match) => ( // match.id may be undefined )); } ``` **Solutions:** ```tsx // ✅ Option 1: Fallback to index (acceptable for append-only lists) { matches.map((match, index) => ( )); } // ✅ Option 2: Generate stable IDs when data arrives (better for reordering) const matchesWithIds = useMemo( () => matches.map((match, i) => ({ ...match, _stableKey: match.id ?? `streaming-${i}`, })), [matches], ); { matchesWithIds.map((match) => ( )); } ``` ## 4. Undefined Nested Properties Nested objects may be partially streamed: ```tsx // ❌ ERROR: "Cannot read properties of undefined (reading 'emblem')" { match.competition.emblem && ; } ``` **Solutions:** ```tsx // ✅ Option 1: Optional chaining throughout { match?.competition?.emblem && ( {match.competition.name} ); } // ✅ Option 2: Guard the entire block { match?.competition && (
{match.competition.name}
); } // ✅ Option 3: Use propStatus to wait for completion const { propStatus } = useTamboStreamStatus(); { propStatus.competition?.isSuccess && ; } ``` ## 5. Arrays Without Safe Access Streamed arrays may be undefined initially: ```tsx // ❌ ERROR: "Cannot read properties of undefined (reading 'map')" { items.map((item) => ); } ``` **Solutions:** ```tsx // ✅ Option 1: Optional chaining on array { items?.map((item, i) => ); } // ✅ Option 2: Default to empty array { (items ?? []).map((item, i) => ); } // ✅ Option 3: Guard with propStatus const { propStatus } = useTamboStreamStatus(); { propStatus.items?.isSuccess && items.map((item) => ); } ``` ## 6. Acting on Incomplete Streamed Data Sometimes you need to trigger an action (API call, validation, etc.) only after a streamed value is complete. Don't act on partial data: ```tsx // ❌ BAD: API called with partial/incomplete data during streaming function SearchComponent({ query }: Props) { const [results, setResults] = useState([]); useEffect(() => { if (query) { // This fires repeatedly as query streams in character by character! fetchSearchResults(query).then(setResults); } }, [query]); } ``` **Solution:** Use `propStatus` to wait for completion: ```tsx // ✅ Only act when the prop is fully streamed function SearchComponent({ query }: Props) { const { propStatus } = useTamboStreamStatus(); const [results, setResults] = useState([]); useEffect(() => { // Only fetch when query is complete, not while streaming if (propStatus.query?.isSuccess && query) { fetchSearchResults(query).then(setResults); } }, [propStatus.query?.isSuccess, query]); return (
{propStatus.query?.isStreaming &&

Generating search query...

} {propStatus.query?.isSuccess && ( <>

Searching for: {query}

{results.map((r) => ( ))} )}
); } ``` For complete component patterns, see [Streaming Best Practices](/concepts/streaming/streaming-best-practices).