Prerequisites
Ionic Framework Version
v8.x
Current Behavior
When a React component forwards an optional prop to an Ionic component and the caller
leaves it unset, @ionic/react writes the string "undefined" into the corresponding DOM
attribute.
const MyToggle: React.FC<{ id?: string }> = ({ id }) => <IonToggle id={id}>Toggle</IonToggle>;
<MyToggle />
renders:
<ion-toggle id="undefined" role="switch" aria-checked="false" aria-labelledby="ion-tg-0-lbl" tabindex="0" class="md toggle-label-placement-start toggle-ltr hydrated">Toggle</ion-toggle>
There is no error and no warning. Consequences:
- Every element rendered this way carries the same id, so any page with more than one has
duplicate ids (invalid HTML), and document.getElementById('undefined') resolves to
whichever comes first.
id is not special. Any prop backed by a reflected DOM property behaves the same way:
title={undefined} produces a tooltip that reads "undefined", and slot={undefined}
places the element in a slot named undefined, which moves it in the layout.
- A prop that had a value and is then set to
undefined is not cleared: the attribute is
overwritten with "undefined" rather than removed.
Expected Behavior
A prop whose value is undefined should not be written to the element at all, so no
attribute appears — the behavior React gives for host elements, and the behavior the
wrapper's own render() already implements. A prop that previously had a value and is set
to undefined should have its attribute removed.
Steps to Reproduce
git clone https://github.com/ptmkenny/ionic-react-router-6-test.git
cd ionic-react-router-6-test && git switch react-attach-props-undefined
npm install && npm run dev
- Open the app at
http://localhost:5173/ (Tab 1). It renders one IonToggle through a
wrapper that forwards an optional id, and prints the attribute the element actually
received.
- Observe: the page reports
id attribute on <ion-toggle>: "undefined" where (none) is
expected. Confirm in devtools that the element is <ion-toggle id="undefined" …>.
- Press Pass id="real-id" — the attribute correctly becomes
"real-id".
- Press Pass id=undefined and observe: the attribute stays
"undefined" instead of
being removed.
Code Reproduction URL
https://github.com/ptmkenny/ionic-react-router-6-test/tree/react-attach-props-undefined
Ionic Info
Ionic:
Ionic CLI : 7.2.1 (/home/node/.npm/_npx/f6fddb685269761d/node_modules/@ionic/cli)
Ionic Framework : @ionic/react 8.7.12-dev.11765219790.17cbe2e9
Capacitor:
Capacitor CLI : 8.0.0
@capacitor/android : not installed
@capacitor/core : 8.0.0
@capacitor/ios : not installed
Utility:
cordova-res : not installed globally
native-run : 2.0.1
System:
NodeJS : v24.19.0 (/usr/local/bin/node)
npm : 11.17.0
OS : Linux 6.18
Additional Information
Analyzed and prepared with Claude Opus.
Root cause
packages/react/src/components/react-component-lib/utils/attachProps.ts assigns every
incoming prop onto the custom element without checking whether it has a value:
(node as any)[name] = newProps[name];
const propType = typeof newProps[name];
if (propType === 'string') {
node.setAttribute(camelToDashCase(name), newProps[name]);
}
id is a reflected non-nullable DOMString IDL attribute, so per WebIDL node.id = undefined stringifies the value and the element ends up with id="undefined". The
typeof === 'string' guard never comes into it — the property assignment has already
reflected to the attribute. The same applies to title, slot, name, lang and dir,
and to any Stencil @Prop() declared with reflect: true.
The wrapper's two prop paths disagree with each other. render() in
createComponent.tsx deliberately filters these values out:
// we should only render strings, booleans, and numbers as attrs in html.
// objects, functions, arrays etc get synced via properties on mount.
const type = typeof value;
if (type === 'string' || type === 'boolean' || type === 'number') {
acc[camelToDashCase(name)] = value;
}
undefined fails that check, so React never emits the attribute — and then attachProps,
called from componentDidMount, writes it anyway.
A second, related gap explains step 7: attachProps receives oldProps but uses it only
for className, so it has no notion of a prop being removed. A removal therefore writes
"undefined" over the previous value instead of clearing it.
Prerequisites
Ionic Framework Version
v8.x
Current Behavior
When a React component forwards an optional prop to an Ionic component and the caller
leaves it unset,
@ionic/reactwrites the string"undefined"into the corresponding DOMattribute.
renders:
There is no error and no warning. Consequences:
duplicate ids (invalid HTML), and
document.getElementById('undefined')resolves towhichever comes first.
idis not special. Any prop backed by a reflected DOM property behaves the same way:title={undefined}produces a tooltip that reads "undefined", andslot={undefined}places the element in a slot named
undefined, which moves it in the layout.undefinedis not cleared: the attribute isoverwritten with
"undefined"rather than removed.Expected Behavior
A prop whose value is
undefinedshould not be written to the element at all, so noattribute appears — the behavior React gives for host elements, and the behavior the
wrapper's own
render()already implements. A prop that previously had a value and is setto
undefinedshould have its attribute removed.Steps to Reproduce
git clone https://github.com/ptmkenny/ionic-react-router-6-test.gitcd ionic-react-router-6-test && git switch react-attach-props-undefinednpm install && npm run devhttp://localhost:5173/(Tab 1). It renders oneIonTogglethrough awrapper that forwards an optional
id, and prints the attribute the element actuallyreceived.
id attribute on <ion-toggle>: "undefined"where(none)isexpected. Confirm in devtools that the element is
<ion-toggle id="undefined" …>."real-id"."undefined"instead ofbeing removed.
Code Reproduction URL
https://github.com/ptmkenny/ionic-react-router-6-test/tree/react-attach-props-undefined
Ionic Info
Additional Information
Analyzed and prepared with Claude Opus.
Root cause
packages/react/src/components/react-component-lib/utils/attachProps.tsassigns everyincoming prop onto the custom element without checking whether it has a value:
idis a reflected non-nullableDOMStringIDL attribute, so per WebIDLnode.id = undefinedstringifies the value and the element ends up withid="undefined". Thetypeof === 'string'guard never comes into it — the property assignment has alreadyreflected to the attribute. The same applies to
title,slot,name,langanddir,and to any Stencil
@Prop()declared withreflect: true.The wrapper's two prop paths disagree with each other.
render()increateComponent.tsxdeliberately filters these values out:undefinedfails that check, so React never emits the attribute — and thenattachProps,called from
componentDidMount, writes it anyway.A second, related gap explains step 7:
attachPropsreceivesoldPropsbut uses it onlyfor
className, so it has no notion of a prop being removed. A removal therefore writes"undefined"over the previous value instead of clearing it.