Skip to content

fix(cli): stop scheduling state updates from inside a state updater - #29211

Open
linhongyu510 wants to merge 2 commits into
google-gemini:mainfrom
linhongyu510:fix/input-history-store-pure-updaters
Open

fix(cli): stop scheduling state updates from inside a state updater#29211
linhongyu510 wants to merge 2 commits into
google-gemini:mainfrom
linhongyu510:fix/input-history-store-pure-updaters

Conversation

@linhongyu510

@linhongyu510 linhongyu510 commented Sep 4, 2026

Copy link
Copy Markdown

Summary

useInputHistoryStore.addInput() scheduled state updates from inside a state
updater: it called setPastSessionMessages() within the
setCurrentSessionMessages() updater, and ran recalculateHistory() — itself a
setState — from that nested updater. React requires updaters to be pure, and
may invoke them more than once per update.

The resulting history was already idempotent, so this fixes a latent contract
violation plus one measurable cost: a redundant render pass on every submit.

Details

The pattern in packages/cli/src/ui/hooks/useInputHistoryStore.ts was:

setCurrentSessionMessages((prevCurrent) => {
  const newCurrentSession = [...prevCurrent, trimmedInput];
  setPastSessionMessages((prevPast) => {
    recalculateHistory(newCurrentSession.slice().reverse(), prevPast);
    return prevPast; // No change to past messages
  });
  return newCurrentSession;
});

The inner updater returned prevPast unchanged purely to piggyback a side
effect and read prevPast.

Instrumenting the current code (counters inside each updater, rendered under
StrictMode) shows a single addInput('hello'):

updater invocations for one submit
outer (setCurrentSessionMessages) 2
inner (setPastSessionMessages) 3
recalculateHistory 3

This change derives the next session list before touching state, then issues
two plain updates. Because addInput can no longer read prevCurrent/prevPast
from an updater, the two lists are mirrored in refs kept in sync at every write
site (addInput and both branches of initializeFromLogger).

Scope notes:

  • The hook's public shape (inputHistory, addInput, initializeFromLogger)
    is unchanged, so AppContainer needs no changes.
  • _currentSessionMessages was already unread before this change (hence the
    underscore prefix). Per review it is now removed entirely, since addInput
    was its only writer; currentRef is the single source for the running
    session list. _pastSessionMessages still has two writers in
    initializeFromLogger, so it stays.

Related Issues

Fixes #29046

How to Validate

npm ci
npm run build --workspace @google/gemini-cli-core   # needed before cli tests resolve
cd packages/cli
npx vitest run src/ui/hooks/useInputHistoryStore.test.ts

Expected: 19 passed. The file had 14 tests; this PR adds 5.

To confirm the new render-count test is load-bearing, restore only the old
addInput body (keeping the new tests) and re-run:

× does not schedule extra renders per submit under StrictMode
  Tests  1 failed | 18 passed (19)

Measured renders for one submit under StrictMode: 6 before → 4 after.

Note on the follow-up commit: removing the dead setCurrentSessionMessages
does not reduce renders further (still 2 per submit — React batches the two
dispatches into one pass). It removes dead state, not a render pass.

Also run, unchanged by this PR:

npx vitest run src/ui/hooks/useInputHistory.test.ts   # 14 passed
npm run typecheck --workspace @google/gemini-cli      # clean
npx eslint packages/cli/src/ui/hooks/useInputHistoryStore.ts --max-warnings 0

Edge cases covered by the added tests: batched submits in one act(),
past-session messages still visible after batched submits, consecutive-duplicate
dedup across batched submits, and per-submit render cost not growing as submits
accumulate (guards a redundant state write from creeping back).

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any) — none; hook API unchanged
  • Validated on required platforms/methods:
    • MacOS

useInputHistoryStore.addInput() called setPastSessionMessages() from
inside the setCurrentSessionMessages() updater, and ran
recalculateHistory() — itself a setState — from that nested updater. The
inner updater returned prevPast unchanged purely to piggyback the side
effect.

React updaters must be pure and may run more than once per update
(StrictMode double-invoke, replays under batching). Instrumenting the
current code shows one addInput invoking the outer updater twice and the
inner updater three times, so recalculateHistory ran three times for a
single submit.

Derive the next session list before touching state and issue two plain
updates, keeping the two lists mirrored in refs so addInput can read them
without an updater. The resulting history was already idempotent, so the
visible fix is the redundant render pass: one submit under StrictMode goes
from 6 renders to 4.

The hook's public shape is unchanged, so AppContainer is unaffected.
@linhongyu510
linhongyu510 requested a review from a team as a code owner September 4, 2026 10:00
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a latent contract violation in useInputHistoryStore where state updates were being scheduled from within other state updaters. By refactoring the logic to derive new state before issuing updates and utilizing refs for synchronization, the implementation now adheres to React's requirement for pure updaters. This change eliminates redundant render passes and improves the stability of the input history management.

Highlights

  • State Update Refactoring: Removed nested state updates within addInput to ensure React updater purity and prevent redundant render passes.
  • State Synchronization: Introduced useRef hooks (pastRef and currentRef) to mirror message lists, allowing state derivation to occur outside of updater functions.
  • Testing Improvements: Added four new test cases to verify render counts under StrictMode and ensure correct history state during batched updates.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions Bot added the size/m A medium sized PR label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 154
  • Additions: +137
  • Deletions: -17
  • Files changed: 2

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the useInputHistoryStore hook to move state updates outside of updater functions, preventing redundant renders and ensuring compatibility with React's StrictMode double-invocations and batching. It introduces useRef mirrors for past and current message lists to derive state updates upfront, and adds comprehensive tests to verify behavior under StrictMode. The reviewer pointed out that calling setCurrentSessionMessages is redundant since currentSessionMessages is a dead state that is never read or returned, and suggested removing it to avoid unnecessary state dispatch overhead.

Comment on lines +106 to +113
const newCurrentSession = [...currentRef.current, trimmedInput];
currentRef.current = newCurrentSession;

return newCurrentSession;
});
setCurrentSessionMessages(newCurrentSession);
recalculateHistory(
newCurrentSession.slice().reverse(), // Convert to newest first
pastRef.current,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

_currentSessionMessages is a dead state that is never read or returned by this hook. Since recalculateHistory already updates the inputHistory state, calling setCurrentSessionMessages is redundant and adds unnecessary state dispatch overhead. Please remove this call.

      const newCurrentSession = [...currentRef.current, trimmedInput];
      currentRef.current = newCurrentSession;

      recalculateHistory(
        newCurrentSession.slice().reverse(),
        pastRef.current,
      );

@gemini-cli gemini-cli Bot added priority/p2 Important but can be addressed in a future release. area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality labels Sep 4, 2026
Review feedback: _currentSessionMessages was never read or returned, so
dispatching to it on every submit was pure overhead. Remove the state pair
and keep currentRef as the single source for the running session list.

Renders per submit are unchanged at 2 - React batches the two dispatches
into one pass - so this removes dead state rather than a render. Adds a
test that the per-submit render cost does not grow as submits accumulate,
so a reintroduced redundant write would show up.
@linhongyu510

Copy link
Copy Markdown
Author

Thanks — verified and applied in 19be911.

Confirmed the claim first: grep -rn "_currentSessionMessages\|currentSessionMessages" packages/cli/src/ returns only the useState declaration itself, so the state really is never read or returned. Since addInput was its only remaining writer, I removed the whole state pair rather than leave an unused setter behind; currentRef is now the single source for the running session list. _pastSessionMessages still has two writers in initializeFromLogger, so that one stays.

One correction on the rationale, so the PR description doesn't overstate the win: renders per submit are unchanged at 2. I measured before and after with a render-counting probe under StrictMode, and React batches the two dispatches into a single pass, so the redundant setCurrentSessionMessages was not costing a render. The change is still right — it removes dead state and a pointless dispatch — but it isn't a render reduction, so I didn't claim one in the commit message.

Added does not grow the render cost as submits accumulate to guard the regression, since a reintroduced per-submit write wouldn't be caught by the existing assertions.

Re-verified after the change:

  • useInputHistoryStore.test.ts + useInputHistory.test.ts → 33 passed (19 + 14)
  • restoring only the original nested-updater addInput still fails does not schedule extra renders per submit under StrictMode (1 failed | 18 passed), so the suite has not lost its grip on the original bug
  • tsc --noEmit clean, eslint --max-warnings 0 clean, prettier --check clean

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality priority/p2 Important but can be addressed in a future release. size/m A medium sized PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(cli): impure state updater in useInputHistoryStore schedules nested setState inside another setState updater

1 participant