# 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 (
);
}
// ✅ 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 && (
);
}
// ✅ 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).