Summary
Reasoning keeps duration in Radix useControllableState, where undefined means "uncontrolled". A caller that only learns the real duration when reasoning finishes naturally passes duration={undefined} while streaming and a number afterwards. React then logs this in development for every finished reasoning block:
undefined is changing from uncontrolled to controlled. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.
(undefined is the hook's caller name, which Reasoning does not pass.)
Source: packages/elements/src/reasoning.tsx, line 78:
const [duration, setDuration] = useControllableState<number | undefined>({
defaultProp: undefined,
prop: durationProp,
});
Reproduction
const [part, setPart] = useState({ streaming: true, seconds: undefined as number | undefined });
// later, when the model finishes thinking:
setPart({ streaming: false, seconds: 3 });
<Reasoning isStreaming={part.streaming} duration={part.seconds}>
<ReasoningTrigger />
<ReasoningContent>…</ReasoningContent>
</Reasoning>
The header renders correctly ("Thought for 3 seconds"), but the warning fires on the transition.
Why the caller can't avoid it
undefined is the only way to say "not known yet", and it is also what makes the prop uncontrolled. The alternatives change behaviour:
- Passing a number while streaming (e.g.
0) turns the component controlled from the start. The internal timer can then never supply a value, and the default getThinkingMessage treats duration === 0 as still thinking.
- Remounting with a
key when streaming ends resets the open state, so a panel the user opened collapses.
Suggested fix
duration has no onChange, so the controllable-state machinery adds nothing here. Keep the internal timer in plain state and let the prop win when it is set:
const [measuredDuration, setDuration] = useState<number | undefined>(undefined);
const duration = durationProp ?? measuredDuration;
This gives the same value useControllableState produces in both modes (the prop when it is defined, otherwise the internal timer), without the mode switch. We ship this as a local patch and it has been fine.
Summary
Reasoningkeepsdurationin RadixuseControllableState, whereundefinedmeans "uncontrolled". A caller that only learns the real duration when reasoning finishes naturally passesduration={undefined}while streaming and a number afterwards. React then logs this in development for every finished reasoning block:(
undefinedis the hook'scallername, whichReasoningdoes not pass.)Source:
packages/elements/src/reasoning.tsx, line 78:Reproduction
The header renders correctly ("Thought for 3 seconds"), but the warning fires on the transition.
Why the caller can't avoid it
undefinedis the only way to say "not known yet", and it is also what makes the prop uncontrolled. The alternatives change behaviour:0) turns the component controlled from the start. The internal timer can then never supply a value, and the defaultgetThinkingMessagetreatsduration === 0as still thinking.keywhen streaming ends resets the open state, so a panel the user opened collapses.Suggested fix
durationhas noonChange, so the controllable-state machinery adds nothing here. Keep the internal timer in plain state and let the prop win when it is set:This gives the same value
useControllableStateproduces in both modes (the prop when it is defined, otherwise the internal timer), without the mode switch. We ship this as a local patch and it has been fine.