Skip to content

Conversation

@ia319
Copy link
Member

@ia319 ia319 commented Nov 13, 2025

Closes #32997

What I did

After identifying that the error was caused by LeafNode becoming undefined, I added logs in code/core/src/manager/components/sidebar/Tree.tsx.

The logs show:

Initial data prop: {..., example-button--primary: {parameters: {…}, ..., prepared: true}}
Item passed to Node: {parameters: {…}, ..., prepared: true}

These logs indicate that incomplete data was passed to the Node component — the item object was missing its type property.
As a result, const LeafNode = ...[item.type] evaluated to undefined, causing the render error.

Root Cause
The updateStory function incorrectly created an incomplete entry in the index when handling an update event for a story that had been filtered out.

Fix
Added an existence check in updateStory so it only updates entries that already exist.

Testing

Manual testing

(added by @Sidnioulz)

  1. Open Storybook and select a story you know how to filter
  2. Apply a filter to exclude the current story
  3. Open DevTools or resize window to force a switch to the mobile layout
  4. Attempt to open the sidebar menu button

Without the PR, I confirm a React crash occurs. With the PR, the sidebar opens (which incidentally clears filters due to #32997 which I'm working on).

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

The error described in the issue has disappeared, and no new errors have been found.

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved story update stability with enhanced validation checks to prevent errors during modifications.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 13, 2025

📝 Walkthrough

Walkthrough

The updateStory function adds defensive guards to prevent unconditional writes to index and filteredIndex properties. Updates to index[storyId] and filteredIndex[storyId] now only proceed if the target entries already exist, ensuring no writes to undefined entries.

Changes

Cohort / File(s) Summary
Defensive update guards
code/core/src/manager-api/modules/stories.ts
Added existence checks before updating index and filteredIndex entries in the updateStory function. Both conditional branches (with and without ref) now follow the same defensive pattern.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

  • Specific areas for attention:
    • Verify that the existence checks cover all necessary conditions to prevent undefined mutations
    • Confirm that both ref and non-ref branches have identical guard logic
    • Ensure the persistence logic via store.setState remains functionally equivalent

Possibly related PRs

✨ Finishing touches
  • 📝 Generate docstrings

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

🧹 Nitpick comments (1)
code/core/src/manager-api/modules/stories.ts (1)

602-602: Update or remove the now-addressed FIXME comment.

This FIXME describes the exact issue being fixed in this PR. Consider updating it to document the fix or removing it entirely.

-// FIXME: is there a bug where filtered stories get added back in on updateStory???
+// updateStory only updates entries that already exist to prevent corruption of filtered indexes
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0e575e and 6e35108.

📒 Files selected for processing (1)
  • code/core/src/manager-api/modules/stories.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier formatting on changed files before committing
Run ESLint on changed files and fix all errors/warnings before committing (use yarn lint:js:cmd <file>)

Files:

  • code/core/src/manager-api/modules/stories.ts
**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions from modules when they need to be unit-tested

Files:

  • code/core/src/manager-api/modules/stories.ts
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In application code, use Storybook loggers instead of console.* (client code: storybook/internal/client-logger; server code: storybook/internal/node-logger)

Files:

  • code/core/src/manager-api/modules/stories.ts
{code/**,scripts/**}/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/manager-api/modules/stories.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.
📚 Learning: 2025-11-05T09:38:47.712Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/components/components/Select/Select.tsx:200-204
Timestamp: 2025-11-05T09:38:47.712Z
Learning: Repo: storybookjs/storybook — Guidance: Until Storybook 11 is released, do not suggest using React.useId anywhere (e.g., in code/core/src/components/components/Select/Select.tsx) to maintain compatibility with React 17 runtimes. Prefer advising: accept a caller-provided props.id and, if needed, generate a client-only fallback id to minimize SSR hydration issues — but avoid useId. Resume prompting for useId after Storybook 11.

Applied to files:

  • code/core/src/manager-api/modules/stories.ts
📚 Learning: 2025-09-18T20:51:06.618Z
Learnt from: Sidnioulz
Repo: storybookjs/storybook PR: 32458
File: code/core/src/viewport/components/Tool.tsx:38-39
Timestamp: 2025-09-18T20:51:06.618Z
Learning: The useGlobals hook from storybook/manager-api returns a tuple where the third element (storyGlobals) is typed as Globals, not Globals | undefined. This means TypeScript guarantees it's always defined, making the `in` operator safe to use without additional null checks.

Applied to files:

  • code/core/src/manager-api/modules/stories.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (1)
code/core/src/manager-api/modules/stories.ts (1)

608-624: LGTM! Defensive guards prevent index corruption.

The existence checks ensure that updateStory only modifies entries that already exist in both index and filteredIndex, preventing the creation of incomplete entries for filtered-out stories. This directly addresses the root cause described in the PR objectives.

Comment on lines 625 to 640
} else {
const { id: refId, index, filteredIndex }: any = ref;
index[storyId] = {
...index[storyId],
...update,
} as API_StoryEntry;
filteredIndex[storyId] = {
...filteredIndex[storyId],
...update,
} as API_StoryEntry;
if (index && index[storyId]) {
index[storyId] = {
...index[storyId],
...update,
} as API_StoryEntry;
}
if (filteredIndex && filteredIndex[storyId]) {
filteredIndex[storyId] = {
...filteredIndex[storyId],
...update,
} as API_StoryEntry;
}
await fullAPI.updateRef(refId, { index, filteredIndex });
}
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 | 🔴 Critical

🧩 Analysis chain

LGTM! Consistent defensive guards across both branches.

The ref branch now has the same existence checks as the no-ref branch, ensuring consistent behavior. However, the updateDocs function (lines 642-676) still performs unconditional updates without existence checks.

Verify whether updateDocs can encounter the same filtered-entry scenario and needs similar defensive guards for consistency:


🏁 Script executed:

#!/bin/bash
# Description: Check how updateDocs is called and whether it can receive updates for filtered-out docs entries

# Search for calls to updateDocs to understand the calling patterns
rg -nP --type=ts -C3 '\bupdateDocs\s*\(' 

# Search for DOCS_PREPARED event handling to see if similar filtering scenarios apply
rg -nP --type=ts -C5 'DOCS_PREPARED'

Length of output: 19635



Wait, this will need cloning. Let me revise:




🏁 Script executed:

#!/bin/bash
# Search for updateDocs function definition and all its usages
rg -nP --type=ts 'updateDocs' code/core/src/manager-api/modules/stories.ts

Length of output: 125


🏁 Script executed:

#!/bin/bash
# Search for updateDocs function definition and all its usages in the stories module
rg -nP --type=ts 'updateDocs'

Length of output: 215



---

🏁 Script executed:

```shell
#!/bin/bash
# Extract the updateDocs implementation
sed -n '642,676p' code/core/src/manager-api/modules/stories.ts

Length of output: 997


Add defensive existence checks to updateDocs ref branch for consistency.

The updateStory fix correctly adds existence checks in the ref branch. However, updateDocs's ref branch (lines 664-669) unconditionally updates index[docsId] and filteredIndex[docsId] without verifying those entries exist, mirroring updateStory's pre-fix vulnerability. Apply the same defensive pattern:

} else {
  const { id: refId, index, filteredIndex }: any = ref;
  if (index && index[docsId]) {
    index[docsId] = { ...index[docsId], ...update } as API_DocsEntry;
  }
  if (filteredIndex && filteredIndex[docsId]) {
    filteredIndex[docsId] = { ...filteredIndex[docsId], ...update } as API_DocsEntry;
  }
  await fullAPI.updateRef(refId, { index, filteredIndex });
}
🤖 Prompt for AI Agents
In code/core/src/manager-api/modules/stories.ts around lines 625 to 640, the ref
branch of updateDocs unconditionally mutates index[docsId] and
filteredIndex[docsId]; add the same defensive existence checks used in
updateStory: guard with if (index && index[docsId]) before merging update into
index[docsId], and if (filteredIndex && filteredIndex[docsId]) before merging
into filteredIndex[docsId], then call await fullAPI.updateRef(refId, { index,
filteredIndex }) as before.

Copy link
Member Author

Choose a reason for hiding this comment

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

Currently, I haven’t noticed any errors caused by it, but it might still be necessary. Awaiting suggestions.

@nx-cloud
Copy link

nx-cloud bot commented Nov 13, 2025

View your CI Pipeline Execution ↗ for commit aed740f


☁️ Nx Cloud last updated this comment at 2025-12-10 20:19:39 UTC

@storybook-app-bot
Copy link

storybook-app-bot bot commented Nov 13, 2025

Package Benchmarks

Commit: aed740f, ran on 10 December 2025 at 20:34:12 UTC

No significant changes detected, all good. 👏

@valentinpalkovic valentinpalkovic changed the title fix(core): prevent updating non-existent stories Sidebar: Prevent updating non-existent stories Nov 17, 2025
Copy link
Member

@Sidnioulz Sidnioulz left a comment

Choose a reason for hiding this comment

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

Code LGTM but I don't feel like I understand why this would fix the reported issue.

I also near systematically get Error 327 on your branch (and on next). So I'm thinking the PR addresses a symptom and not a root cause of the bugs related to switching between mobile and desktop layouts.

@ia319 could you please share more details on why this code change fixes the issue? How have you tested this? Did you systematically see the issue on next and is it systematically fixed? Or does it occur less often?

@ia319
Copy link
Member Author

ia319 commented Nov 19, 2025

Code LGTM but I don't feel like I understand why this would fix the reported issue.

I also near systematically get Error 327 on your branch (and on next). So I'm thinking the PR addresses a symptom and not a root cause of the bugs related to switching between mobile and desktop layouts.

@ia319 could you please share more details on why this code change fixes the issue? How have you tested this? Did you systematically see the issue on next and is it systematically fixed? Or does it occur less often?

Regarding why you still get the error on this branch, I suspect it’s an Nx caching issue. I tried multiple times, and on this branch the problem no longer occurs. The error only reappears when I switch back to next and rebuild.

Previously, I found that the crash was caused by the LeafNode variable in the Tree.tsx component becoming undefined.

To diagnose the issue, I added logs in the treeItems map function of Tree.tsx:

const item = collapsedData[itemId];
console.log('Item passed to Node:', item);

When the bug occurred, the log showed that an incomplete data object (missing the type property) was being passed to the Node component:

Item passed to Node: {parameters: {…}, initialArgs: {…}, argTypes: {…}, args: {…}, prepared: true}

This incomplete data caused the LeafNode lookup logic ({ docs: DocumentNode, story: StoryNode }[item.type]) to fail and return undefined.

@ia319
Copy link
Member Author

ia319 commented Nov 19, 2025

I tried multiple times, and on this branch the problem no longer occurs. The error only reappears when I switch back to next and rebuild.

In storybook:ui and sandbox , I repeatedly followed the reproduction steps described in the issue. After that, I monitored the page and the console and did not observe the error again.

@github-actions github-actions bot added the Stale label Dec 1, 2025
@Sidnioulz
Copy link
Member

Thanks for clarifying @ia319!

I suspect that we should not try to fix build caching issues with changes to our code. To the best of my understanding, those build caching issues only occur in our monorepo (and contributors usually learn that they will want to purge dependencies and build cache if their branch has significantly diverged from next; I personally use git clean -xdf && yarn task compile).

I'm gonna pass this PR on to someone more experienced to see if they agree, but my recommendation would be to close the PR. My reasoning is that adding conditions could end up making it harder to diagnose future bugs, as we'd get missing stories instead of early errors during index computation.

@ia319
Copy link
Member Author

ia319 commented Dec 8, 2025

Thanks for clarifying @ia319!

I suspect that we should not try to fix build caching issues with changes to our code. To the best of my understanding, those build caching issues only occur in our monorepo (and contributors usually learn that they will want to purge dependencies and build cache if their branch has significantly diverged from next; I personally use git clean -xdf && yarn task compile).

I'm gonna pass this PR on to someone more experienced to see if they agree, but my recommendation would be to close the PR. My reasoning is that adding conditions could end up making it harder to diagnose future bugs, as we'd get missing stories instead of early errors during index computation.

Thanks for the detailed explanation. I'll go ahead and close this PR, and hope we can identify the root cause.

@ia319 ia319 closed this Dec 8, 2025
@ia319
Copy link
Member Author

ia319 commented Dec 9, 2025

@Sidnioulz
Hi I think I should explain this, because there may have been some misunderstanding earlier.

Regarding the “cache issue” I mentioned: I wasn't saying that what I fixed was the cache issue.
What I meant was: when you said you could still encounter Error 327 on my fix branch, I believe that specific error was caused by the NX cache in your local monorepo (clearing the cache should resolve it).
But my PR is intended to fix another completely different issue — a runtime crash that is consistently reproducible.

As for why I made this change, I want to apologize first.
When I previously explained the fix, I only mentioned the surface-level symptom (LeafNode being undefined), and didn’t clearly explain the underlying root cause.

Root cause

When the user uses tag filtering, stories that do not match (such as example-button--primary) are correctly removed from filteredIndex entirely.
At this point, filteredIndex[id] becomes undefined. I believe this is correct, because that story should not be shown.

Where the bug happens

When a background incremental update is triggered for that story, the current updateStory logic blindly performs the merge(code/core/src/manager-api/modules/stories.ts#L627):

filteredIndex[id] = { ...filteredIndex[id], ...update };

Since filteredIndex[id] is undefined, the result of the merge becomes a “partial object” containing only the update fields and missing core fields (such as type).

This story, which should have been hidden, gets added back into the index in a corrupted form.
When the UI (the Tree component) tries to render it, because the type field is missing, it cannot match the correct component, which eventually causes the LeafNode is undefined crash.

What this fix does

My change adds a check:
If a story has already been filtered out (i.e., it is not present in filteredIndex), then we should ignore incremental updates for it and not add it back.

Clarification about the reproduction steps

You must first select the stories that are about to be filtered, and then perform the reproduction steps.

After filtering, you can also reproduce the issue by modifying the label of the stories that still remain on the page, which triggers a hot update.

Why selecting first is necessary,I believe the reason is here:

code/core/src/preview-api/modules/preview-web/PreviewWithSelection.tsx#L202

At the moment the update is triggered, it is still referencing the stories before filtering, and when those filtered-out stories receive an update, the update logic breaks.

@Sidnioulz
Copy link
Member

Sidnioulz commented Dec 10, 2025

Thank you so much for the explanation, @ia319! I'll reopen the PR and do a new review.

I want to emphasize the importance of following the PR template. There is a section asking you what automatic tests you wrote, and a mandatory section explaining how to manually test the fix. The manual testing section makes things a lot clearer for reviewers, and it avoids this kind of misunderstanding. It's the most important part of the template :)

Also, you are now allowed to create branches in the storybookjs org repo. Feel free to do that. It makes it easier for us to check out your branches and test your PRs. Thanks!

@Sidnioulz Sidnioulz reopened this Dec 10, 2025
@Sidnioulz Sidnioulz self-requested a review December 10, 2025 08:11
@ia319
Copy link
Member Author

ia319 commented Dec 10, 2025

I want to emphasize the importance of following the PR template. There is a section asking you what automatic tests you wrote, and a mandatory section explaining how to manually test the fix. The manual testing section makes things a lot clearer for reviewers, and it avoids this kind of misunderstanding. It's the most important part of the template :)

Thank you for your reply. I did not intend to cause any misunderstanding regarding the manual testing section in the template. The part mentioned above,

Clarification about the reproduction steps

is something I discovered after revisiting it in my research yesterday.

Copy link
Member

@Sidnioulz Sidnioulz left a comment

Choose a reason for hiding this comment

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

Finally managed to repro, can confirm the bug is fixed. Kudos @ia319! 🎉

@Sidnioulz Sidnioulz removed the Stale label Dec 10, 2025
@valentinpalkovic valentinpalkovic merged commit 8a46fe2 into storybookjs:next Dec 15, 2025
58 of 59 checks passed
@github-actions github-actions bot mentioned this pull request Dec 15, 2025
18 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants