react: [RFC] onChange -> onInput, and don't polyfill onInput for uncontrolled components

onChange is a nicer name for what onInput does and the fact that it has propagated up to other high-level components as the default name is much nicer than onInput as a high level event name.

Generally it has been helpful for the many new-comers to React that don’t know the DOM well (which is a lot more than the inverse). However, that doesn’t change the fact that it can be confusing for people that are familiar.

Unfortunately, changing it now would cause confusion for everyone that already knows React.

The reason I’d like to change it now is because I’d like to get away from polyfilling it for uncontrolled components. This use case is filled with all kinds of imperative code which leads to edge cases. E.g. reading/setting e.target.value or reading/setting ref.value.

When you use controlled components you shouldn’t need to touch them imperatively and therefore won’t hit the edge cases. Ideally we should get away from reading from e.target.value and instead just pass the value directly to the event handler.

Proposal:

Controlled Components

  • onInput: Polyfilled and works like onChange does today. It is allowed to over-fire many events even if nothing changed. May have special Fiber rules regarding synchronous flushing. Optional: Pass value as second arg.
  • onChange: Works like onInput for one version but warns about being deprecated and suggests switching to onInput. In next version it works like the browser but still warns and tells you to use onInput forever.

Optional: Add a getter/setter on DOM .value in development mode and warn if this is used directly.

Uncontrolled Components

  • onInput: Not polyfilled. Works however the browser works. Warns about browser differences if you don’t also specify onClick, onKeyDown and/or onKeyUp. The warnings suggests implementing those listeners to cover more edge cases, or switch to a controlled component.
  • onChange: Not polyfilled. Works however the browser works.

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 39
  • Comments: 19 (9 by maintainers)

Most upvoted comments

I really the general concept of this change. It allows simplifying a really complicated polyfill while also actively encouraging folks to use the better controlled input pattern. I’d really encourage ya’ll to please give a full major version for this to warn, and not add it in v15.6. The entire “form” library ecosystem in React is built on top of the behavior of onChange, and custom inputs most copy the onChange/value pattern.

IMO losing access to e.target would be a regression - I’d like to keep having access to the element and it’s props, which can be useful (access selection range, data-* props etc)

Another reason is access to input.validity which is needed for input validation.

@sebmarkbage

When you use controlled components you shouldn’t need to touch them imperatively and therefore won’t hit the edge cases. Ideally we should get away from reading from e.target.value and instead just pass the value directly to the event handler.

IMO losing access to e.target would be a regression - I’d like to keep having access to the element and it’s props, which can be useful (access selection range, data-* props etc)

Wondering if you guys going to proceed in this onChange/onInput matter ?

I think in the long-term we probably want to move away from adding more event namespaces to ReactDOM, including the changing of onChange and onInput. A better approach would be to move this custom ReactDOM logic to a separate element/node. Doing so would allow us to move forward with higher-level abstractions without them conflicting with the existing and future DOM namespaces on the web platform.

Any update on this issue?

It seems to me that, instead of waiting for this issue to be resolved, you can simply write a custom input component, which exposes the DOM-based change event. This is how I implemented it (with TypeScript):

import { Component, createElement, InputHTMLAttributes } from 'react';

export interface CustomInputProps {
    onChange?: (event: Event) => void;
    onInput?: (event: Event) => void;
}

/**
 * This custom input component restores the 'onChange' and 'onInput' behavior of JavaScript.
 */
export class CustomInput extends Component<Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onInput' | 'ref'> & CustomInputProps> {
    private readonly registerCallbacks  = (element: HTMLInputElement | null) => {
        if (element) {
            element.onchange = this.props.onChange ? this.props.onChange : null;
            element.oninput = this.props.onInput ? this.props.onInput : null;
        }
    };

    public render() {
        return <input ref={this.registerCallbacks} {...this.props} onChange={undefined} onInput={undefined} />;
    }
}

Please let me know if you see ways to improve this approach or encounter problems with it. I’m still gaining experience with this CustomInput component. For example, checkboxes behave strangely. I either have to invert event.target.checked in the onChange handler while passing the value to the checkbox with checked or can get rid of this inversion when passing the value to the checkbox with defaultChecked but this then breaks that several checkboxes representing the same state in different places on the page keep in sync with the state. (In both cases, I didn’t pass an onInput handler to the CustomInput for checkboxes.)

@sebmarkbage I’ve previously brought up separating uncontrolled and controlled inputs entirely and you guys seemed to generally agree, might this be a good time for that too or is that no longer relevant? I.e. <input> is uncontrolled and <FooBarInput> is a React-enhanced implementation with controlled behavior. Could possibly even be its own package? IMHO if we’re going with removing attribute whitelists then not special-casing native inputs seems like a step in the same direction.

Regarding onChange vs onInput. Unless there are IME-issues with hiding onInput then that seems fine. But onChange seems like a generally good React naming convention for components that change, also, checkboxes/radios etc would still report their value change via onChange and not onInput right?