# refactor\(web\): typing no longer re-renders the composer model picker · PR \#22721 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22721)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: refactor/chat-composer-stable-controls

Target branch: refactor/chat-composer-footer-controls

Head commit: eb67900ea5954f10f1a98adeac3f7c12716a099a

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/22721)

[Commits](https://git.cafe/pingdotgg/t3code/pull/22721/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/22721/diffs?format=markdown)

## Description

---

Every keystroke in the composer re-rendered the model picker and the attach button, even though neither depends on the prompt.

The fix uses component boundaries, not stable-callback wrappers. This adds no `useStableCallback` uses; ChatComposer keeps the 34 it has on main.

- **`ComposerModelPicker`** (in `ComposerFooterControls.tsx`) wraps `ProviderModelPicker`. It defines the single-model and multi-model toggle handlers itself, from the multi-model selections and their setter, `selectedModelSelection`, the instance entries, the model options and `onProviderModelSelect`. None of those change while you type, so the compiler keeps the handlers. ChatComposer builds the `<ComposerModelPicker>` element as its own const. The controls row around it is rebuilt on every keystroke because `TraitsPicker` takes `prompt`, but the picker element itself stays cached.
- **`ComposerAttachButton`** owns the hidden file input and builds its change handler from `addComposerAttachments` and `focusComposer`.

| | main | this PR |
|---|---|---|
| ChatComposer.tsx lines | 6,700 | 6,640 |
| Renders, 20 typed chars | 678 (22 commits) | **352** (22 commits) |
| ProviderModelPicker subtree renders | 21 + popover/tooltip children | 0 |
| Attach button renders | 21 (button, tooltip, icon) | 21 (`ComposerAttachButton` alone) |

Renders measured with the shared fiber counter (`rc-init.js`) on a seeded dev server: typing 20 characters into an existing thread's composer, median of 3 runs.

Still rendering on each keystroke:
- `ComposerAttachButton` itself. `addComposerAttachments` gets a new identity on every keystroke because of the prompt-editing helpers it calls. Its tooltip, button and icon children no longer re-render.
- `TraitsPicker`. It takes `prompt` to detect "ultrathink", which needs a change to TraitsPicker itself. Both are left for follow-ups.

Tests run: `vp lint` and `vp fmt --check` on the changed files, `tsc-rs --noEmit` for apps/web, and `reactCompilerCoverage.test.ts` for the changed files (0 bailouts).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

