Ionic-framework: bug: @ionic/react: ion-slides: Failed to execute 'removeChild' on 'Node'

Created on 12 Jul 2019  路  15Comments  路  Source: ionic-team/ionic-framework

Bug Report

Ionic version:
[cli] 5.2.2
[@ionic/react] 0.0.6

Current behavior:
When trying to remove a slide to dynamically generated slides, the following uncaught exception occurs:

react-dom.development.js:9279 Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

Expected behavior:
The slides should be updated to reflect the correct number of slides, without causing any errors.

Steps to reproduce:

  1. Start a new React blank project:
    ionic start slides blank --type=react

  2. Use the following Home.tsx template:

import {IonContent, IonHeader, IonTitle, IonToolbar, IonSlides, IonSlide, IonButton} from '@ionic/react';
import React, {useState} from 'react';

const slideOpts = {
    initialSlide: 0,
    speed: 400
};

const Home: React.FunctionComponent = () => {

    const [slides, setSlides] = useState([
        {id: '1', text: 'Slide 1'},
        {id: '2', text: 'Slide 2'},
        {id: '3', text: 'Slide 3'},
        {id: '4', text: 'Slide 4'},
        {id: '5', text: 'Slide 5'}
    ]);

    return (
        <>
            <IonHeader>
                <IonToolbar>
                    <IonTitle>Ionic Blank</IonTitle>
                </IonToolbar>
            </IonHeader>
            <IonContent>

                <IonButton onClick={() => setSlides(slides.slice(1))}>
                    Remove first slide
                </IonButton>

                <IonSlides pager={true} options={slideOpts}>
                    {slides.map(slide => (<IonSlide key={slide.id}><h1>{slide.text}</h1></IonSlide>))}
                </IonSlides>
            </IonContent>
        </>
    );
};

export default Home;
  1. Run ionic serve
  2. Click on the "Remove first slide" button.

Related code:

https://github.com/m-spyratos/-ionic-react-slides-remove

Ionic info:

Ionic:

   Ionic CLI: 5.2.2 (/usr/local/lib/node_modules/ionic)
   Ionic Framework: @ionic/react 0.0.6

Utility:

   cordova-res: not installed
   native-run: not installed

System:

   NodeJS: v10.16.0 (/usr/local/bin/node)
   npm: 6.10.0
   OS: macOS Mojave
react triage

Most helpful comment

Here is a workaround. Change the IonSlides key when your slides list changes. It's not a universal workaround but it's suitable for some cases:

// When the `item` slides ids don't change
<IonSlides key={item.id}>
  {item.slides.map(slide => (<IonSlide key={slide.id}><h1>{slide.text}</h1></IonSlide>))}
</IonSlides>

// When the `item` slides ids change
<IonSlides key={item.slides.map(slide => slide.id).join('_')}>
  {item.slides.map(slide => (<IonSlide key={slide.id}><h1>{slide.text}</h1></IonSlide>))}
</IonSlides>

It makes the ion-slides be destroyed and created (instead of crashing) on every slides change.

All 15 comments

I'm experiencing a the same issue. Triggered when navigation between two screens having dynamically generated slides.

I don't know if it helps, but in React, I had the same issue with Materialize Stepper and solved it wrapping the step-content component by <React.Fragment> instead <div>.

Although ionic is not react-based, the ideia is the <React.Fragment> is kind a "neutral" component that does not inject invalid nodes in the DOM as div could do.

Maybe it is related.

@lfernando-silva Thanks for the suggestion, but this doesn't work for me unfortunately.

const slides = groups.map((group) => (
  <IonSlide key={group.name}>
    Slide
  </IonSlide>
));
<IonSlides
  pager={false}
  options={slideOpts}
  ref={_slidesRef}
  onIonSlideDidChange={() => handleSlideChange()}
>
  {slides}
</IonSlides>

If i wrap the slides in a div <div>{slides}</div> it breaks the IonSlides but the error goes away. However just wrapping it in a <Fragment> doesn't do anything.

I was meaning to change the IonSlide component behavior to "inherit" a Fragment-like component.

I don't know if it is possible, but for example, in the React Material-UI Button Component, you can pass a component param to change the behavior of the Button, where the default is the HTML button. Passing a instead, the Button component acts as a HTML <a> tag.

The IonSlide is not very well documented, but as far as I can tell from looking through the code this isn't possible.

Here is a workaround. Change the IonSlides key when your slides list changes. It's not a universal workaround but it's suitable for some cases:

// When the `item` slides ids don't change
<IonSlides key={item.id}>
  {item.slides.map(slide => (<IonSlide key={slide.id}><h1>{slide.text}</h1></IonSlide>))}
</IonSlides>

// When the `item` slides ids change
<IonSlides key={item.slides.map(slide => slide.id).join('_')}>
  {item.slides.map(slide => (<IonSlide key={slide.id}><h1>{slide.text}</h1></IonSlide>))}
</IonSlides>

It makes the ion-slides be destroyed and created (instead of crashing) on every slides change.

@Finesse
Thanks for this, can't believe I didn't think of that. This works for my case and I guess for now this will be the solution 馃憤

Thanks for the issue! Is this still an issue using the official Ionic React version, the latest being 5.1.1 at the time of writing?

This is still an issue with 5.0.7.

Can confirm this issue

I just replaced my IonSlides by react-id-swiper and it works like a charm.

@brandyscarney I can also confirm this is still an issue on 5.1.1

Any chance this ticket can get an update? This issue is actually quite crippling to ionic display functionality.

Still experiencing this. Link to a CodeSandbox;

Search Miami, select the city, and wait for the weather to load. Then search Berlin, select it, and wait for the weather to load. The application will crash, and the error message pinpoints it to <IonSlide> being the culprit. As the commenter above said, this is CRIPPLING to functionality, _especially_ something as basic as a dynamically-rendered carousel.

In case anyone needs it, I created a simple tik-tok state machine which seems to work well:

const Swiper: React.FC<SwiperArguments> = (
    {
        elements,
    }) => {

    const elementReducer = (state: { elements: any[], tick: boolean }, action: { type: "set_elems" | "tok", elements: any[] }) => {
        switch (action.type) {
            case "tok":
                return {...state, tick: false};
            case "set_elems":
                return {...state, elements: action.elements, tick: true}
            default:
                return state;
        }
    }

    const [data, elementsDispatch] = useReducer(elementReducer, {elements: [], tick: false});

    useEffect(() => {
        elementsDispatch({type: "set_elems", elements: elements});
    }, [elements]);

    useEffect(() => {
        if (data.tick) {
            elementsDispatch({type: "tok", elements: []});
        }
    }, [data.tick])

    return (
        <div className="slider-container">
            {(data.tick && (
                <div className="spinner-wrapper">
                    <IonSpinner/>
                </div>
            )) || (
                <IonSlides>
                    { data.elements.map(((value, index) => {
                      ...
                    }))}
                </IonSlides>
            )}
        </div>
    );
}

It basically removes the swiper for one tick before rebuilding it completely every time the elements change.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

gio82 picture gio82  路  3Comments

GeorgeAnanthSoosai picture GeorgeAnanthSoosai  路  3Comments

vswarte picture vswarte  路  3Comments

brandyscarney picture brandyscarney  路  3Comments

brandyscarney picture brandyscarney  路  3Comments