[Bug]: Accordion crashes for scalar `defaultOpenItems={0}` (headless and styled v9)
Maintainer thường phản hồi trong vòng 2 ngày
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 72/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- react, typescript
- Lĩnh vực
- frontend
Hướng nghiên cứu
Bắt đầu từ phần khởi tạo state dùng chung trong useAccordionBase_unstable thuộc react-accordion, nơi defaultOpenItems được bảo vệ bằng kiểm tra truthy trước khi truyền vào useControllableState. Kiểm tra cách useControllableState trong react-utilities xử lý defaultState không phải hàm, và cách useAccordionItem gọi openItems.includes. Hoàn thành khi defaultOpenItems={0} và {false} mở đúng mục tương ứng mà không ném lỗi, khớp với hành vi của mảng [0], và các test Accordion hiện có vẫn pass.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
An Accordion with a valid scalar default item value of 0 (defaultOpenItems={0}) throws during its initial render:
TypeError: ctx.openItems.includes is not a function
The equivalent array form, defaultOpenItems={[0]}, renders correctly and opens the item.
This reproduces through the public headless Accordion API in
@fluentui/[email protected]. It also reproduces through the public
styled Accordion in @fluentui/[email protected]. Both paths use the same state
initialization logic.
Scalar false produces the same error. This is a runtime bug with supported values, not a
TypeScript error or a requirement to use an array: the public type explicitly accepts a scalar
or an array.
TLDR;
- ❌ Fails:
defaultOpenItems={0}- Throws during initial rendering instead of opening the item withvalue={0}. - ❌ Fails:
defaultOpenItems={false}- Throws during initial rendering instead of opening the item withvalue={false}. - ❌ Fails on interaction:
defaultOpenItems=""- Initially opens the item withvalue="", but its first close attempt leaves it open withcollapsible. Withmultiplealso enabled, that click throwspreviousOpenItems.filter is not a function. - ✅ Works:
defaultOpenItems={[0]}- Opens the item withvalue={0}. - ✅ Works:
defaultOpenItems={[false]}- Opens the item withvalue={false}. - ✅ Works:
defaultOpenItems={1}- Opens the item withvalue={1}. - ✅ Works:
defaultOpenItems="section"- Opens the item withvalue="section". - ✅ Works:
defaultOpenItems={[]}- Starts with no items open. - ✅ Works:
defaultOpenItems={[""]}- Opens the item withvalue=""and closes it correctly withcollapsible. - ✅ Works:
defaultOpenItemsomitted - Starts with no items open. - ✅ Works:
defaultOpenItems={undefined}- Starts with no items open. - ✅ Works:
openItems={0}- Opens the item withvalue={0}in controlled mode. - ✅ Works:
openItems={false}- Opens the item withvalue={false}in controlled mode. - ✅ Works:
openItems={[]}- Keeps all items closed in controlled mode.
Each opening example assumes a matching AccordionItem.value. openItems={false} identifies
the false-valued item; it does not mean "close everything." Controlled changes require updating
openItems through onToggle.
Tested environment
| Item | Version / environment |
|---|---|
@fluentui/react-headless-components-preview |
0.3.2 |
Resolved @fluentui/react-accordion |
9.12.5 |
Resolved @fluentui/react-utilities |
9.27.0 |
| React | 18.3.1 |
| React DOM | 18.3.1 |
| TypeScript | 5.5.4 |
| Node.js | 22.23.1 |
| Browser | Playwright Chromium 151.0.7922.34 |
| Operating system | Windows |
| Rendering paths tested | Client rendering and renderToString |
| Build modes tested | Development client/SSR and production SSR |
The same truthiness check is present in the repository's master source when inspected on
2026-11-02. I have not built master or tested other published package versions.
Minimal reproduction: no browser or app scaffold required
-
In an empty folder, initialize a package and install the versions below:
npm init -y npm install --save-exact [email protected] [email protected] @fluentui/[email protected] @fluentui/[email protected] @fluentui/[email protected]The underlying Accordion and utility packages are pinned to match the tested environment.
The headless preview otherwise resolves them through dependency ranges. -
Save the following as
repro.cjs:const React = require('react'); const { renderToString } = require('react-dom/server'); const { Accordion, AccordionItem, AccordionHeader, AccordionPanel, } = require('@fluentui/react-headless-components-preview/accordion'); const tree = React.createElement( Accordion, { defaultOpenItems: 0, collapsible: true }, React.createElement( AccordionItem, { value: 0 }, React.createElement(AccordionHeader, null, 'Section zero'), React.createElement(AccordionPanel, null, 'Panel content'), ), ); console.log(renderToString(tree)); -
Run:
node repro.cjs -
Observe the exception before the Accordion renders.
-
Change only the Accordion prop:
{ defaultOpenItems: [0], collapsible: true } -
Run the script again. It now returns HTML, and the header has
aria-expanded="true".
Reproduce through styled Fluent v9
Change only the import in the same script:
} = require('@fluentui/react-accordion');
Scalar 0 still throws. The array [0] still works. This confirms that the defect is shared
Accordion behavior rather than a headless-specific styling or rendering layer.
Minimal client reproduction
Use this component in a React 18.3.1 app with the package versions above:
import {
Accordion,
AccordionItem,
AccordionHeader,
AccordionPanel,
} from '@fluentui/react-headless-components-preview/accordion';
export default function App() {
return (
<Accordion defaultOpenItems={0} collapsible>
<AccordionItem value={0}>
<AccordionHeader>Section zero</AccordionHeader>
<AccordionPanel>Panel content</AccordionPanel>
</AccordionItem>
</Accordion>
);
}
- Mount
App. - The Accordion subtree fails during initial rendering with
ctx.openItems.includes is not a function. - Replace
defaultOpenItems={0}withdefaultOpenItems={[0]}. - The component mounts and the header reports
aria-expanded="true".
The client failure was verified in Chromium with an error boundary used only to observe the
exception. The reproduction uses public components, not internal hooks, deprecated navigation
props, or custom wrappers. No provider, custom CSS, icons, or component-specific polyfill is
required to reproduce the crash.
Expected behavior
defaultOpenItems={0}behaves likedefaultOpenItems={[0]}.- The item with
value={0}starts open. - Its header has
aria-expanded="true". - The Accordion renders without throwing.
- Scalar and array forms use the same normalized internal array state.
- Other supported scalar values are not treated as absent just because they are falsy.
Actual behavior
The uncontrolled state stores the scalar value 0 instead of an array such as [0].
AccordionItem then calls .includes() on that number, causing the initial render to fail.
Relevant stack frames from the Node reproduction, with local path prefixes omitted:
TypeError: ctx.openItems.includes is not a function
at ...\@fluentui\react-accordion\lib-commonjs\components\AccordionItem\useAccordionItem.cjs:17:84
at ...\@fluentui\react-accordion\lib-commonjs\contexts\accordion.cjs:31:152
at useContextSelector (...\@fluentui\react-context-selector\lib-commonjs\useContextSelector.cjs:22:27)
This is not only a development warning. Production SSR throws the same exception.
Observed input matrix
The item value matches the supplied default or controlled value in each row. For an omitted,
undefined, or empty-array value, the item value is 0.
| Prop | Observed result | Verification |
|---|---|---|
defaultOpenItems={0} |
Throws: ctx.openItems.includes is not a function |
Client and SSR |
defaultOpenItems={false} |
Throws the same error | Client and SSR |
defaultOpenItems={null} |
Throws: Cannot read properties of null (reading 'includes') |
SSR |
defaultOpenItems={[0]} |
Renders; matching item starts open | Client and SSR |
defaultOpenItems={[false]} |
Renders; matching item starts open and closes with collapsible |
Client and SSR |
defaultOpenItems={1} |
Renders; matching item starts open | SSR |
defaultOpenItems="section" |
Renders; matching item starts open | SSR |
openItems={0} |
Renders; matching item starts open | Client and SSR |
openItems={false} |
Renders; matching item starts open | Client and SSR |
openItems={[]} |
Renders; all items stay closed in controlled mode | SSR |
defaultOpenItems={[]} |
Renders; item starts closed | SSR |
defaultOpenItems omitted |
Renders; item starts closed | Client and SSR |
defaultOpenItems={undefined} |
Renders; item starts closed | SSR |
defaultOpenItems="", with collapsible |
Renders open; first close attempt incorrectly keeps the item open | Client and SSR for initial rendering; client for interaction |
defaultOpenItems="", with collapsible and multiple |
Renders open; clicking throws previousOpenItems.filter is not a function |
Client |
defaultOpenItems={[""]}, with collapsible |
Renders open; clicking closes the matching item correctly | Client and SSR |
The 0 failure also reproduces in production SSR with all four combinations of
multiple={false/true} and collapsible={false/true}. The corresponding [0] controls work in
all four combinations.
The null result is included as an additional observation. Numeric 0 alone demonstrates the
bug without relying on unusual item values.
Public type contract
The installed API allows scalar values:
defaultOpenItems?: Value | Value[];
openItems?: Value | Value[];
AccordionItemValue is unknown. Both of these assignments type-check against the public
headless export with TypeScript 5.5.4, strict checking, and no casts:
import type { AccordionProps } from '@fluentui/react-headless-components-preview/accordion';
const numericDefault: AccordionProps = { defaultOpenItems: 0 };
const booleanDefault: AccordionProps = { defaultOpenItems: false };
The client example above also type-checks without casts.
Root cause
The shared useAccordionBase_unstable passes this expression to useControllableState:
defaultState: defaultOpenItems && (() => initializeUncontrolledOpenItems({ defaultOpenItems, multiple })),
JavaScript's && returns the left operand when it is falsy. For scalar 0, the expression
therefore passes defaultState: 0, not an initializer or undefined. For scalar false, it
passes defaultState: false.
useControllableState uses initialState only when defaultState === undefined. A provided
non-function default is stored directly. The resulting openItems is therefore a number or
boolean rather than an array.
The existing initializeUncontrolledOpenItems already knows how to wrap a scalar in an array.
The truthiness guard prevents that function from running for falsy scalar values.
The controlled openItems path does not have the same problem: it calls normalizeValues,
which checks for undefined explicitly and wraps provided scalar values. This explains why
openItems={0} works while defaultOpenItems={0} fails.
AccordionItem subsequently evaluates:
ctx.openItems.includes(value)
That call throws when the unnormalized default is a number or boolean.
Source references
Exact published sources used for the reproduction:
- Shared Accordion state initialization, react-accordion 9.12.5
- AccordionItem context lookup, react-accordion 9.12.5
- Controllable state initialization, react-utilities 9.27.0
- Headless public hook delegation, preview 0.3.2
- Public headless Accordion declarations, preview 0.3.2
Repository source:
The source snippets above are from Microsoft's MIT-licensed Fluent UI implementation.
They identify the cause; consumers do not need to call these internal hooks to reproduce it.
Suggested fix direction
Check whether a default was provided, rather than whether it is truthy. For example:
defaultState:
defaultOpenItems !== undefined
? () => initializeUncontrolledOpenItems({ defaultOpenItems, multiple })
: undefined,
This preserves the omitted-default path and sends provided scalar values through the existing
normalization function. It also preserves the existing initialization policy for arrays and
multiple.
This is a suggested upstream change, not a patch applied in the consumer project.
Suggested regression coverage
- Uncontrolled scalar
0renders and opens the matching item. - Uncontrolled scalar
falserenders and opens the matching item. - Uncontrolled scalar
""initializes array state and closes on its first click with
collapsible, both with and withoutmultiple. The current code renders initially but fails
that interaction, so a render-only test would not prove normalization. - Scalar and array forms produce equivalent initial expanded state and toggle results.
- Omitted
defaultOpenItemsretains the existing initial closed state. - Controlled scalars
openItems={0}andopenItems={false}continue to work, including toggle
callbacks and caller-managed state updates. - Exercise
multipleandcollapsiblecombinations. - Cover the shared base behavior and at least one public headless-component regression, since
the same code is used by styled Fluent v9 and the headless facade.
Impact and temporary workaround
Numeric IDs starting at 0 are common when generating Accordion items from data. Selecting the
first such item through the documented scalar default form crashes the affected component
subtree instead of rendering a valid initial state. The same failure prevents SSR from returning
the Accordion markup.
Consumers can temporarily pass an array without changing the item value:
<Accordion defaultOpenItems={[0]}>
<AccordionItem value={0}>
<AccordionHeader>Section zero</AccordionHeader>
<AccordionPanel>Panel content</AccordionPanel>
</AccordionItem>
</Accordion>
For a boolean item value, the corresponding workaround is defaultOpenItems={[false]}.
This does not require converting the component to controlled state or renaming valid item IDs.
- Ngôn ngữ chính
- TypeScript
- Star
- 20.4k
- Fork
- 3k
- Merge trung bình
- 4 ngày 1 giờ
- Pull request đã merge (30 ngày)
- 100
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Không có hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của microsoft/fluentui
-
[Bug]: Button shims in react-migration-v8-v9 always render the ms-Button--primary classCó thể đã có người làm @jakesalvesen đã nhận 14 ngày trước. Đang mởComponent: Button Fluent UI react-components (v9) Type: Bug :bug:
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 2 ngày
-
[Bug]: AreaChart renders SVG rect with negative width and height on initial renderCó thể đã có người làm @AtishayMsft đã nhận 11 ngày trước. Đang mởPackage: charting Type: Bug :bug:
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
microsoft/fluentui#36976 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 2 ngày
-
[Bug]: Vertical Slider still logs the deprecated 'slider-vertical' warning in Chromium 132+Có thể đã có người làm @jakesalvesen đã nhận 17 ngày trước. Đang mởComponent: Slider Fluent UI react-components (v9) Type: Bug :bug:
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
microsoft/fluentui#36945 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 2 ngày
-
[Bug]: fluent-checkbox submits "on" when the checked attribute is before valueCó thể đã có người làm @FOWEPJF255 đã nhận 11 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 2 ngày
-
[Bug]: fluent-slider initial value is not submitted with the formCó thể đã có người làm @FOWEPJF255 đã nhận 11 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Maintainer thường phản hồi trong vòng 2 ngày
Tất cả issue của microsoft/fluentui
Issue tương tự
-
Navbar avatar and name stay stale after saving profile changesCó thể đã có người làm @alexechoi đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
marmelab/atomic-crm#416 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
epam/ai-dial-react-file-manager#99 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
canonical/craft-actions#146 ·
Maintainer thường phản hồi trong vòng 2 ngày
-
feature-request
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
bluesky-social/social-app#12343 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
lucide-icons/lucide#5165 ·
Maintainer thường phản hồi trong vòng 1 ngày