Skip to content

fix(actions): prevent state mutation and retain newest actions in ActionLogger#34053

Open
tysoncung wants to merge 1 commit intostorybookjs:nextfrom
tysoncung:fix/action-logger-state-mutation
Open

fix(actions): prevent state mutation and retain newest actions in ActionLogger#34053
tysoncung wants to merge 1 commit intostorybookjs:nextfrom
tysoncung:fix/action-logger-state-mutation

Conversation

@tysoncung
Copy link

@tysoncung tysoncung commented Mar 6, 2026

What

Fix three bugs in the ActionLogger addAction callback:

  1. State mutation via previous.count++ — directly mutates the previous state object, violating React's immutability principle. This can cause subtle reconciliation bugs where React doesn't detect the state change.

  2. Incoming object mutation via action.count = 1 — mutates the incoming action object rather than creating a copy. This could affect other references to the same object.

  3. slice(0, limit) keeps oldest, discards newest — when the action log reaches its limit, the oldest entries are retained and new ones are dropped. Changed to slice(-limit) to keep the most recent actions, which is the expected behavior for an action log.

How

  • When merging with a duplicate previous entry, create a new object with { ...previous, count: previous.count + 1 } instead of mutating previous.count.
  • When adding a new entry, create a copy with { ...action, count: 1 } instead of mutating action.count.
  • Use slice(-limit) instead of slice(0, limit) to retain the newest actions.

Testing

Verified the fix addresses all three checklist items from #34052. The changes are minimal and focused on the state updater function.

Closes #34052

Summary by CodeRabbit

  • Refactor
    • Updated data handling in the action logger component through improved implementation patterns to enhance code maintainability and reliability.

…ionLogger

Fix three bugs in the ActionLogger addAction callback:

1. previous.count++ mutates the previous state object directly,
   violating React's immutability principle. Now creates a new object
   with the incremented count.

2. action.count = 1 mutates the incoming action object. Now creates
   a shallow copy with count set to 1.

3. slice(0, limit) retains the oldest actions and discards newest
   when the limit is reached. Changed to slice(-limit) to keep the
   most recent actions, which is the expected behavior for an action
   log.

Fixes storybookjs#34052
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

This change refactors the ActionLogger component to adopt an immutable update pattern instead of mutating action objects in-place. The logic for determining previous action data and conditional updates remains functionally equivalent, but now creates new objects when updating the last entry or appending new actions.

Changes

Cohort / File(s) Summary
ActionLogger Immutability Refactor
code/core/src/actions/containers/ActionLogger/index.tsx
Replaced in-place mutation of the last action object's count with immutable updates using new object creation. Updated array handling to avoid mutating existing entries while maintaining the same conditional logic for incrementing counts or appending new actions.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/core/src/actions/containers/ActionLogger/index.tsx`:
- Around line 44-47: Guard against non-positive or undefined limits before
calling slice(-action.options.limit): validate and normalize
action.options.limit to a non-negative integer (e.g., parse to Number, floor,
and clamp to >=0) and then branch on that value so you only call slice(-limit)
when limit > 0; when limit === 0 return an empty array (or otherwise enforce the
intended zero-behavior) instead of using slice(-0) or slice(-undefined). Apply
this for both uses in the block that references
updated.slice(-action.options.limit) and [...prevActions,
newAction].slice(-action.options.limit), using the normalized limit variable and
the newAction/prevActions/updated symbols to locate the code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f95661c9-0757-48c3-87e9-5c5e203bc437

📥 Commits

Reviewing files that changed from the base of the PR and between fd2919d and 0ba4e35.

📒 Files selected for processing (1)
  • code/core/src/actions/containers/ActionLogger/index.tsx

Comment on lines +44 to +47
return updated.slice(-action.options.limit);
} else {
action.count = 1;
newActions.push(action);
const newAction = { ...action, count: 1 };
return [...prevActions, newAction].slice(-action.options.limit);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find ActionsParameters type definition and limit property
rg -n "ActionsParameters|limit" code/core/src/actions --type=ts --type=tsx -A 5 -B 2 | head -100

# Also search more broadly for the type definition
rg -n "interface.*ActionsParameters|type.*ActionsParameters" . --type=ts --type=tsx

Repository: storybookjs/storybook

Length of output: 92


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for explicit zero-limit examples/configuration
rg -n "limit\s*:\s*0|limit.*=.*0" code/core/src --type=ts --type=tsx

# Look for limit validation or guards
rg -n "limit.*[<>].*0|if.*limit" code/core/src/actions --type=ts --type=tsx -A 2 -B 1

Repository: storybookjs/storybook

Length of output: 92


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the actual file in question for context
cat -n code/core/src/actions/containers/ActionLogger/index.tsx | head -60

Repository: storybookjs/storybook

Length of output: 2466


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the types file - look for it in the actions directory
find code/core/src/actions -name "types*" -o -name "index.ts" | head -20

Repository: storybookjs/storybook

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for ActionsParameters definition across the codebase
rg -n "ActionsParameters" code/core/src --max-count=20 -A 3 -B 1

Repository: storybookjs/storybook

Length of output: 1427


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for limit property within types or interfaces
rg -n "limit.*:" code/core/src/actions -A 1 -B 1 | head -80

Repository: storybookjs/storybook

Length of output: 480


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the types.ts file to see the full ActionsParameters definition
cat -n code/core/src/actions/types.ts

Repository: storybookjs/storybook

Length of output: 2315


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the ActionOptions model for limit type and any constraints
cat -n code/core/src/actions/models/ActionOptions.ts

Repository: storybookjs/storybook

Length of output: 440


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check configureActions for more context on limit and validation
cat -n code/core/src/actions/runtime/configureActions.ts

Repository: storybookjs/storybook

Length of output: 397


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any validation or guards on limit in the codebase
rg -n "limit.*[>0<]|if.*limit|limit.*\|\|" code/core/src/actions -B 2 -A 2

Repository: storybookjs/storybook

Length of output: 830


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if limit can be undefined or if action.options.limit is always defined
# Look at ActionDisplay type definition
rg -n "ActionDisplay" code/core/src/actions -A 5 -B 1 | head -80

Repository: storybookjs/storybook

Length of output: 6227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if there are any tests that set limit to 0 or negative values
rg -n "limit.*0|limit.*-" code/core --max-count=30

Repository: storybookjs/storybook

Length of output: 1646


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check ActionDisplay interface to see how options are structured
cat -n code/core/src/actions/models/ActionDisplay.ts

Repository: storybookjs/storybook

Length of output: 338


🏁 Script executed:

#!/bin/bash
set -eulo pipefail

# Check if there's any validation or type narrowing that ensures limit is positive
rg -n "ActionDisplay.*count|options.*limit" code/core/src/actions -B 3 -A 3 | head -100

Repository: storybookjs/storybook

Length of output: 1198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the runtime/action.ts to see how actionOptions is constructed and if limit is always set
cat -n code/core/src/actions/runtime/action.ts | sed -n '80,105p'

Repository: storybookjs/storybook

Length of output: 1124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if config.limit is always applied or if there's fallback handling
rg -n "config\." code/core/src/actions/runtime/action.ts -B 2 -A 2

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Let me trace how actionOptions are passed to the action function
rg -n "export.*action\s*=" code/core/src/actions/runtime/action.ts -A 3 -B 3

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if limit can be undefined - look at how ActionOptions is actually used
rg -n "actionOptions\|ActionOptions" code/core/src/actions/runtime/action.ts -B 2 -A 2

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for where the limit is actually resolved or if there's a fallback
rg -n "\.limit|limit\s*\?" code/core/src/actions -B 1 -A 1 | head -60

Repository: storybookjs/storybook

Length of output: 700


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Let me check more precisely how action.options.limit is set
# by looking at the full action function signature
cat -n code/core/src/actions/runtime/action.ts | sed -n '40,100p'

Repository: storybookjs/storybook

Length of output: 2418


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if limit value is ever validated or constrained elsewhere
rg -n "if.*limit\|limit.*?.*:|limit.*>|limit.*<" code/core/src/actions

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Double-check by looking at the complete ActionOptions type definition
cat -n code/core/src/actions/models/ActionOptions.ts

Repository: storybookjs/storybook

Length of output: 440


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what happens when slice is called with -0
node <<'EOF'
const arr = [1, 2, 3, 4, 5];
console.log("slice(-50):", arr.slice(-50));      // Should give [1,2,3,4,5]
console.log("slice(-0):", arr.slice(-0));        // Equivalent to slice(0)
console.log("slice(0):", arr.slice(0));          // Gives full array
console.log("[] with slice(-0):", [].slice(-0)); // Empty array
EOF

Repository: storybookjs/storybook

Length of output: 173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if there are any integration tests or stories that verify the limit=0 behavior
rg -n "limit.*0|limit:.*0" code/core/src/actions --type=js -B 2 -A 2

Repository: storybookjs/storybook

Length of output: 509


Guard non-positive or undefined limit before using slice(-limit).

Lines 44 and 47 use slice(-action.options.limit) without validation. Since ActionOptions is Partial<Options>, the limit property can be undefined. Additionally, limit has no type constraint enforcing positive integers. When limit is 0 or undefined, slice(-0) and slice(-undefined) both return the full array, silently disabling log truncation instead of keeping zero or all entries as expected. Add a guard to handle these edge cases explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/actions/containers/ActionLogger/index.tsx` around lines 44 -
47, Guard against non-positive or undefined limits before calling
slice(-action.options.limit): validate and normalize action.options.limit to a
non-negative integer (e.g., parse to Number, floor, and clamp to >=0) and then
branch on that value so you only call slice(-limit) when limit > 0; when limit
=== 0 return an empty array (or otherwise enforce the intended zero-behavior)
instead of using slice(-0) or slice(-undefined). Apply this for both uses in the
block that references updated.slice(-action.options.limit) and [...prevActions,
newAction].slice(-action.options.limit), using the normalized limit variable and
the newAction/prevActions/updated symbols to locate the code.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Potential logic bug with options.limit in Actions addon

1 participant