Ionic-framework: bug: changing value of component in ionChange results in an infinite loop

Created on 18 Dec 2019  路  15Comments  路  Source: ionic-team/ionic-framework

Bug Report

Ionic version:
[x] 4.7.11

Current behavior:
When the value of an Ionic input element is changed from OUTSIDE of the element, the element's onIonChange is triggered, thereby duplicating the change event.

Expected behavior:
As with every other React input, the onIonChange should only be triggered when the value was change from the element itself.

Steps to reproduce:
See console in included stackblitz.

  1. Both the IonInput and regular input are controlled via the same value.
  2. When changing it from the IonInput, only the onIonChange is triggered.
  3. When changing it from the regular input, the input's onChange is called AS WELL AS the onIonChange from the IonInput.

Related code:
https://stackblitz.com/edit/ionic-v4-11-7-controlled-inputs

Other information:
This same bug is happening with other Ionic inputs as well. I haven't tested all of them, but it is happening with at least IonToggle and IonDateTime.

This is bad because unless each input has its own separate controlled state, doing something like clearing a state object could trigger 10+ onIonChange events, leading to race conditions and other annoyances.

Ionic info:

Ionic:

   Ionic CLI       : 5.4.13 (/Users/evan/.config/yarn/global/node_modules/ionic)
   Ionic Framework : @ionic/react 4.11.7

Capacitor:

   Capacitor CLI   : 1.4.0
   @capacitor/core : 1.4.0

Utility:

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

System:

   NodeJS : v13.1.0 (/usr/local/Cellar/node/13.1.0/bin/node)
   npm    : 6.12.1
   OS     : macOS Catalina
core bug

Most helpful comment

And as a followup, it seems that other inputs don't have onIonInput or anything like it that only triggers for inputs via that element. For example, IonDateTime and IonToggle only have onIonChange. What are we supposed to do in those cases?

All 15 comments

ionChange is triggered with every change of the input value, either through user input or programmatically. If you want your handler to be triggered only when the user changes the input, you should use ionInput event instead.

@masimplo Thanks for that clarification; I now see what happened. The event types for onIonInput may be broken. I tried using onIonInput and was getting Property 'value' does not exist on type 'EventTarget', but if I declare event: any it does work.

And as a followup, it seems that other inputs don't have onIonInput or anything like it that only triggers for inputs via that element. For example, IonDateTime and IonToggle only have onIonChange. What are we supposed to do in those cases?

Hi, I have the same problem, this is my organization :

  • App (contains filters state and update methods)

    • Menu (contains filters form)

    • FirstPage (display Menu and results)

    • SecondPage (display another filters form and results)

I have 2 forms that use a single state and when I change a value of a field from one form, it trigger the update method on both forms. (this is a problem because I fetch data on each update)

How can I trigger only one time the update method ? onIonInput is not available on IonSegment too...

My first example is not really explicit because I can use differents filters on differents pages but I have another example where I haven't found a solution :

  • Form (keep all states of input in one object and contains update method)

    • Step 1

    • Step 2

At the end of the form I want to reset it, but if I do it it just reset some fields because they all interacts with the same object and re-trigger the update method for each input... The only workaround I have is to use one state for each input and when reset the form, call the setter of each state 馃槥

EDIT:
For those who would have the same problem as me: to avoid state modification conflicts when reacting it is possible with useState to modify from the previous state.

Example:

setSomething({
  ...something,
  anyProperty: value
})

becomes:

setSomething(previousState => ({
  ...previousState,
  anyProperty: value
}))

@ebk46, Hi Evan. It seems there is not any good document or sample code for Ionic-React. Can you guide me, please?

ionChange is triggered with every change of the input value, either through user input or programmatically. If you want your handler to be triggered only when the user changes the input, you should use ionInput event instead.

Same problem exist about onIonInput event

Any ideas how to deal with that? I have IonSelect that is wired with redux state and when logic other than user interacting with that component is updating the values the ionOnChange event is triggered. How to know where the change is coming from?

onIonInput is working for me (4.11.10), but somehow when i change the value (type=date or type=time) outside of the onIonInput handler, it doesn't rerender with the new value.

This is quite an annoying "feature". Additionally, onIonChange calls the function from the previous render, which makes the issue even more annoying.

My current workaround is to keep a useRef reference that is updated on every render. Then when onIonChange is called, you can ignore the call if the new value is the same as the value in useRef.

Note that this feature ruins use-cases where one input field depends on another input field. You can end up in a weird update loop and the end state becomes completely unpredictable.

There is no onIonInput for IonSelect so I can't reset the value from outside.

It actually works when using useEffect as below:

useEffect(() => {
    validate();
  }, [email, password]);

  const changeEmail = (e: any) => {
    setEmail(e.detail.value);
  };

  const changePassword = (e: any) => {
    setPassword(e.detail.value);
  };

  const validate = () => {
    if (email === "" || password === "") {
      setInputValid(false);
    } else {
      setInputValid(true);
    }
  };

The problem is, that when checking for state-changes right after setFoo(newValue), the state didn't actually change yet. useEffect() can be triggered, when a state changes ... so, all you have to do is tell useEffect() in an array, which changes it should listen to.

Hope this helps.
Cheers!

This affects most components that have ionChange, resulting in some pretty annoying infinite loops when used this way.

I had filed https://github.com/ionic-team/ionic-framework/issues/22365, but then realized this issue already existed. Here is a copy of my bug report with an up to date reproduction:

Bug Report

Ionic version:


[ ] 4.x
[x] 5.x

Current behavior:
When changing the value of an Ionic component via an ionChange callback, an infinite loop occurs because changing that value triggers another ionChange callback. This affects any component that has an ionChange event but this issue comes up more often in ion-toggle, ion-checkbox, and ion-input.

Expected behavior:

I would expect programatically changing the value in the ionChange callback would not trigger another ionChange, similar to how the native checkbox element works.

Steps to reproduce:

  1. Open this CodePen: https://codepen.io/liamdebeasi/pen/MWejVLE
  2. Open dev tools and switch to the console.
  3. Click the "Ionic Checkbox". Notice that this causes a new ionChange handler to be called every 1000ms.
  4. Click the "Native Checkbox". Notice that this causes the change event to only be called once.

Other information:

Changing this behavior is likely a breaking change.

What about leave ionChange as it is, for backward compactibility. And provide new event 'change' or something similar with fixed behaviour?

Next step: Events like @change in vuejs can be linked to this new fixed event, but when somebody uses @ionChange, they will be facing same issue till next release of Ionic v6, as mentioned bellow.

Next step: In v6 version this names can be replaced, for smoother transition...

Because it looks, this won't be fixed in short future.

Was this page helpful?
0 / 5 - 0 ratings