Typescript input onchange event.target.value - reactjs

In my react and typescript app, I use:
onChange={(e) => data.motto = (e.target as any).value}
How do I correctly define the typings for the class, so I wouldn't have to hack my way around the type system with any?
export interface InputProps extends React.HTMLProps<Input> {
...
}
export class Input extends React.Component<InputProps, {}> {
}
If I put target: { value: string }; I get :
ERROR in [default] /react-onsenui.d.ts:87:18
Interface 'InputProps' incorrectly extends interface 'HTMLProps<Input>'.
Types of property 'target' are incompatible.
Type '{ value: string; }' is not assignable to type 'string'.

Generally event handlers should use e.currentTarget.value, e.g.:
const onChange = (e: React.FormEvent<HTMLInputElement>) => {
const newValue = e.currentTarget.value;
}
You can read why it so here (Revert "Make SyntheticEvent.target generic, not SyntheticEvent.currentTarget.").
UPD: As mentioned by #roger-gusmao ChangeEvent more suitable for typing form events.
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
}

the correct way to use in TypeScript is
handleChange(e: React.ChangeEvent<HTMLInputElement>) {
// No longer need to cast to any - hooray for react!
this.setState({temperature: e.target.value});
}
render() {
...
<input value={temperature} onChange={this.handleChange} />
...
);
}
Follow the complete class, it's better to understand:
import * as React from "react";
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
interface TemperatureState {
temperature: string;
}
interface TemperatureProps {
scale: string;
}
class TemperatureInput extends React.Component<TemperatureProps, TemperatureState> {
constructor(props: TemperatureProps) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
// handleChange(e: { target: { value: string; }; }) {
// this.setState({temperature: e.target.value});
// }
handleChange(e: React.ChangeEvent<HTMLInputElement>) {
// No longer need to cast to any - hooray for react!
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature} onChange={this.handleChange} />
</fieldset>
);
}
}
export default TemperatureInput;

You can do the following:
import { ChangeEvent } from 'react';
const onChange = (e: ChangeEvent<HTMLInputElement>)=> {
const newValue = e.target.value;
}

ChangeEvent<HTMLInputElement> is the type for change event in typescript. This is how it is done-
import { ChangeEvent } from 'react';
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};

we can also use the onChange event fire-up with defined types(in functional component) like as follows:
const handleChange = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
const name = e.target.name;
const value = e.target.value;
};

as HTMLInputElement works for me

The target you tried to add in InputProps is not the same target you wanted which is in React.FormEvent
So, the solution I could come up with was, extending the event related types to add your target type, as:
interface MyEventTarget extends EventTarget {
value: string
}
interface MyFormEvent<T> extends React.FormEvent<T> {
target: MyEventTarget
}
interface InputProps extends React.HTMLProps<Input> {
onChange?: React.EventHandler<MyFormEvent<Input>>;
}
Once you have those classes, you can use your input component as
<Input onChange={e => alert(e.target.value)} />
without compile errors. In fact, you can also use the first two interfaces above for your other components.

I use something like this:
import { ChangeEvent, useState } from 'react';
export const InputChange = () => {
const [state, setState] = useState({ value: '' });
const handleChange = (event: ChangeEvent<{ value: string }>) => {
setState({ value: event?.currentTarget?.value });
}
return (
<div>
<input onChange={handleChange} />
<p>{state?.value}</p>
</div>
);
}

When using Child Component We check type like this.
Parent Component:
export default () => {
const onChangeHandler = ((e: React.ChangeEvent<HTMLInputElement>): void => {
console.log(e.currentTarget.value)
}
return (
<div>
<Input onChange={onChangeHandler} />
</div>
);
}
Child Component:
type Props = {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
export Input:React.FC<Props> ({onChange}) => (
<input type="tex" onChange={onChange} />
)

An alternative that has not been mentioned yet is to type the onChange function instead of the props that it receives. Using React.ChangeEventHandler:
const stateChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
console.log(event.target.value);
};

Here is a way with ES6 object destructuring, tested with TS 3.3.
This example is for a text input.
name: string = '';
private updateName({ target }: { target: HTMLInputElement }) {
this.name = target.value;
}

This works for me also it is framework agnostic.
const handler = (evt: Event) => {
console.log((evt.target as HTMLInputElement).value))
}

This is when you're working with a FileList Object:
onChange={(event: React.ChangeEvent<HTMLInputElement>): void => {
const fileListObj: FileList | null = event.target.files;
if (Object.keys(fileListObj as Object).length > 3) {
alert('Only three images pleaseeeee :)');
} else {
// Do something
}
return;
}}

Thanks #haind
Yes HTMLInputElement worked for input field
//Example
var elem = e.currentTarget as HTMLInputElement;
elem.setAttribute('my-attribute','my value');
elem.value='5';
This HTMLInputElement is interface is inherit from HTMLElement which is inherited from EventTarget at root level. Therefore we can assert using as operator to use specific interfaces according to the context like in this case we are using HTMLInputElement for input field other interfaces can be HTMLButtonElement, HTMLImageElement etc.
For more reference you can check other available interface here
Web API interfaces by Mozilla
Interfaces in External Node Modules by Microsoft

You no need to type if you do this:
<input onChange={(event) => { setValue(e.target.value) }} />
Because if you set a new value with the arrow function directly in the html tag, typescript will understand by default the type of event.

const handleChange = (
e: ChangeEvent<HTMLInputElement>
) => {
const { name, value } = e.target;
this.setState({ ...currentState, [name]: value });
};
you can apply this on every input element in the form component

function handle_change(
evt: React.ChangeEvent<HTMLInputElement>
): string {
evt.persist(); // This is needed so you can actually get the currentTarget
const inputValue = evt.currentTarget.value;
return inputValue
}
And make sure you have "lib": ["dom"] in your tsconfig.

Convert string to number simple answer
<input
type="text"
value={incrementAmount}
onChange={(e) => {
setIncrementAmmount(+e.target.value);
}}
/>

import { NativeSyntheticEvent, TextInputChangeEventData,} from 'react-native';
// Todo in java script
const onChangeTextPassword = (text : any) => {
setPassword(text);
}
// Todo in type script use this
const onChangeTextEmail = ({ nativeEvent: { text },}: NativeSyntheticEvent<TextInputChangeEventData>) => {
console.log("________ onChangeTextEmail _________ "+ text);
setEmailId(text);
};
<TextInput
style={{ width: '100%', borderBottomWidth: 1, borderBottomColor: 'grey', height: 40, }}
autoCapitalize="none"
returnKeyType="next"
maxLength={50}
secureTextEntry={false}
onChange={onChangeTextEmail}
value={emailId}
defaultValue={emailId}
/>

const event = { target: { value: 'testing' } as HTMLInputElement };
handleChangeFunc(event as ChangeEvent<HTMLInputElement>);
this work for me.

Related

Props "Object is possibly 'undefined'" in React Typescript

I am new to Typescript and I have an error I don't understand in React Typescript. I suspect that it comes from the way I write my interface but I am not sure.
First I call my CellEditable component
<CellEditable value={'Hello'} onChange={() => {}} />
CEllEditable has an isEditable state that toggles InputText on click
CellEditable.tsx
import React, { useState } from 'react'
import Cell from './Cell.comp'
import InputText from './InputText.comp'
interface CellEditableProps {
value: string
onChange?: () => void
}
const renderCellInput = (type: string, opts: any) => {
switch (type) {
case 'text':
return <InputText {...opts} />
default:
return <div>Missing!</div>
}
}
const CellEditable = (props: CellEditableProps) => {
const { value, onChange } = props
const [isEditing, setEditing] = useState<boolean>(false)
const handleClick = () => setEditing(!isEditing)
const handleBlur = () => setEditing(!isEditing)
const opts = {
value,
helpers: {
onBlur: handleBlur,
onChange
}
}
return (
<div onClick={handleClick}>
{
isEditing
? renderCellInput('text', opts)
: <Cell value={value} />
}
</div>
)
}
export default CellEditable
InputText.tsx
import React from 'react'
interface InputTextProps {
value?: string
helpers?: HelpersProps
}
interface HelpersProps {
onChange?: () => void
onBlur?: () => void
}
const InputText = (props: InputTextProps) => {
const { value, helpers } = props
console.log('propsInputText:', props) // Empty object in the console
return (
<input type={'text'} value={value} onChange={helpers.onChange} onBlur={helpers.onBlur} />
)
}
export default InputText
The issue is:
helpers.onChange gets this error "Object is possibly 'undefined'. TS2532"
console.log('propsInputText:', props) in InputText.tsx output an empty object.
Is it an issue with typescript and the way I write my interface?
the helpers property in InputTextProps and the onChange property in your HelpersProps are optional. either make them required by removing the question mark or assign to them a default values when destructuing.
const { value, helpers = {} } = props;
const { onChange = () => {} } = helpers;
return (
<input type={'text'} value={value} onChange={onChange} onBlur={helpers.onBlur} />
)
inside your interface:
interface CellEditableProps {
value: string
onChange?: () => void
}
you placed a ? after onChange, that tells the compiler that it can be not passed and hence you get "Object is possibly 'undefined'
To remedy this solution either you can use onChange with ! like onChange!. This thells compiler that you are sure that onChange will not be null. But this is a bad approach.
What you should do is check if it is not null or undefined and then proceed:
if(onChange) {
...do your stuff here
}
your interface declaration clearly states that these can indeed be undefined (? marks these properties as optional). You'll need to check for their existance or fill them.
const value = props.value || '';
const helpers = {
onChange: () => {},
onBlur: () => {},
...(props.helpers || {}),
};
return (
<input type={'text'} value={value} onChange={helpers.onChange} onBlur={helpers.onBlur} />
)
or similar.

React+typescript : How to change the state if I take it in as an array?

This is my code for a modal form I'm using and I wanted to use my function to console.log, but I'm not sure how to change the state in this.set.state? Any help is appreciated since Im very new to react. The variable st is just the string it takes in each time
interface State {
name: string;
email: string;
amt: string;
}
interface Props {
isOpen: boolean;
handleClose: () => void;
handleSendRequest: (values: State) => void;
}
export default class FormDialog extends React.Component<Props, State> {
state = { name: "", email: "", amt: "" };
onChange = ((st: string) => (event: any) => {
const newObject: any = {};
newObject[st] = event.target.value;
this.setState({ State: newObject[st] }); //this part im struggling with
}).bind(this);
render() {
const { isOpen, handleClose, handleSendRequest } = this.props;
return (
<Dialog
open={isOpen}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">Send Money Request</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
id="standard-read-only-input"
name="name"
label="Contact Name"
defaultValue="John"
onChange={this.onChange("name")}
/>
CodeSandbox example
You can cast newObject as seen below. This answer was derived from this Stack Overflow answer.
onChange = (st: string) => (event: any) => {
const newObject = { [st]: event.target.value } as Pick<State, keyof State>;
this.setState(newObject);
};
EDIT:
In response to: [H]ow it come it doesn't work for default values in the text field? If i take the default tag off and enter a new value it works tho
This is because defaultValue for your input is hard coded. Your component doesn't know about the value you defined in your input. You would have to move the default value into your state and provide the value to the <TextField /> component.
export default class FormDialog extends React.Component<Props, State> {
state = {
name: "John", // Define the default value here
email: "",
amt: "",
};
onChange = (st: string) => (event: any) => {
const newObject = { [st]: event.target.value } as Pick<State, keyof State>;
this.setState(newObject);
};
render() {
return (
<TextField
defaultValue={this.state.name} // Pass the default value as a prop here
onChange={this.onChange("name")}
/>
);
}
}
Here is a simple example in javascript, which will log the state after it has been changed.
class Hello extends React.Component {
constructor(props) {
super(props)
this.state = {
name: ""
}
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({
name: event.target.value
}, () => console.log(this.state));
}
render() {
return <div > < input type = "text"
id = "name"
onChange = {
this.handleChange
}
/></div > ;
}
}
ReactDOM.render( <
Hello / > ,
document.getElementById('container')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>

'TypeError: illegal operation attempted on a revoked proxy' when using Immer with setState

I recently refactored my React App to use Immer. However, in the onFormValueChange using produce gives me the error TypeError: illegal operation attempted on a revoked proxy while the version that is written without produce works fine.
This is the smallest I could reduce the relevant code to :
test.tsx
import { produce } from 'immer';
import { IFormValues, TestForm } from './test_form';
interface ITestProps {}
interface ITestState {
type: string;
}
export class Test extends React.Component<ITestProps, ITestState> {
constructor(props: ITestProps) {
super(props);
this.state = {
type: '',
};
}
handleSubmit = (values: IFormValues) => {
console.log.log(values);
};
onFormValueChange = (values: IFormValues) => {
this.setState(
produce((draft: ITestState) => {
draft.type = values.type;
}),
);
};
// The Following version of the function works perfectly fine and as expected:
//
// onFormValueChange = (values: IFormValues) => {
// this.setState({
// type: values.type,
// });
// };
render() {
let showField = true;
if (this.state.type === 'test') {
showField = false;
}
return (
<div>
<TestForm
submit={this.handleSubmit}
onValueChange={this.onFormValueChange}
>
<input name="type" />
</TestForm>
{showField && this.state.type}
</div>
);
}
}
test_form.tsx
import produce from 'immer';
export interface IFormValues {
[key: string]: any;
}
interface IFormProps {
submit: (values: IFormValues) => void;
onValueChange?: (values: IFormValues) => void;
}
export interface IFormState {
values: IFormValues;
}
interface IFieldProps {
value: any;
name: string;
onChange: (event: any) => void;
}
export class TestForm extends React.Component<IFormProps, IFormState> {
constructor(props: IFormProps) {
super(props);
const values: IFormValues = {};
this.state = {
values,
};
}
private handleSubmit = (event: any) => {
event.preventDefault();
const { submit } = this.props;
const { values } = this.state;
submit(values);
};
handleChange = (event: any) => {
const { name, value } = event.target;
this.setState(
produce((draft: IFormState) => {
draft.values[name] = value;
this.props.onValueChange && this.props.onValueChange(draft.values);
}),
);
};
public render() {
const { values } = this.state;
return (
<form onSubmit={this.handleSubmit} noValidate={true}>
<div>
{React.Children.map(
this.props.children,
(child: React.ReactElement<IFieldProps>) => (
<div>
{React.cloneElement(child, {
value: values[child.props.name],
onChange: this.handleChange,
})}
</div>
),
)}
<div>
<button type="submit">Submit</button>
</div>
</div>
</form>
);
}
}
Caveat: I've never used Immer. But the error is quite clear: You're trying to use a revoked Proxy. My guess is the fundamental problem is here:
this.setState(
produce((draft: IFormState) => {
draft.values[name] = value;
this.props.onValueChange && this.props.onValueChange(draft.values); // <=======
}),
);
In produce, you're passing draft.values into a function that will call produce a second time and put values.type on a different draft state. My guess is you're not allowed to pass data out of the original produce call in that way. (The documentation says "Warning: please note that the draft shouldn't be 'leaked' from the async process and stored else where. The draft will still be revoked as soon as the async process completes." but that warning is in relation to async producers, and yours aren't async. Still, it may be that it's a more general warning, it just happens to be in the async producers part.)
If so, this change to handleChange in TestForm would fix it:
this.setState(
produce((draft: IFormState) => {
draft.values[name] = value;
}),
() => {
this.props.onValueChange && this.props.onValueChange(this.state.values);
}
);
That ensures that it calls onValueChange with the value after the state has been set (presumably it's a normal object at that point, not a proxy).

Warning when passing a function to a wrapped component prop

At present I have a ValidatedTextField component that wraps a TextField component and takes in a validationerror property that is used to communicate between the child and parent and consumed by either the onChange or onBlur event of the textbox.
However when passing a function to this attribute I receive the following error:
Invalid value for prop validationerror
on tag. Either remove it from the element, or pass a string or number value to keep
it in the DOM. For details,
see https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html#changes-in-detail
I have read through the link which says that data and aria attributes can still be passed freely, however switching to using data attribute results in the same error. I cannot think how else to send the function to update the parents error back.
From the wrapper
<ValidatedTextField
type="text"
variant="standard"
required={true}
validateon={'onChange'}
validate={[Validations.Required, Validations.allowedNameCharacters]}
validationerror={(validationError: boolean) => this.setState({ HasError: validationError }) }
onChange={(event: any) => this.setState({ textboxvalue: event.target.value })}
value={this.state.textboxvalue}
/>
and the wrapped component
import * as React from 'react';
import * as _ from 'lodash'
import { IValidationItem } from '../../../Interfaces/IValidationItem'
import TextField, { TextFieldProps } from "#material-ui/core/TextField";
interface IValidatedTextFieldProps {
validate?: IValidationItem[],
validateon?: 'onBlur' | 'onChange',
validationerror?: (hasError?: boolean) => void
}
interface IValidatedTextFieldState {
validationErrorMessage: string,
validationError: boolean
}
type ValidatedTextFieldAllProps = IValidatedTextFieldProps & TextFieldProps
class ValidatedTextField extends React.Component<ValidatedTextFieldAllProps, IValidatedTextFieldState> {
public constructor(props: ValidatedTextFieldAllProps) {
super(props);
this.state = {
validationErrorMessage: "",
validationError: false
}
}
public validationWrapper = (event: any) => {
const { validate, } = this.props;
return !validate ? "" : _.forEach(validate, (validationItem: IValidationItem) => {
const result = !validationItem.func(event.target.value)
if (result) {
this.setState({ validationErrorMessage: validationItem.validationMessage });
this.setState({ validationError: result })
this.callParentValidationErrorMethod(result)
return false;
}
else {
this.setState({ validationErrorMessage: "" });
this.setState({ validationError: result })
this.callParentValidationErrorMethod(result)
return;
}
});
};
public onBlurValidation = (event: any) => {
const { onBlur, validateon, validate } = this.props;
if (_.isFunction(onBlur)) { onBlur(event); }
if (validateon === "onBlur" && !!validate) { this.validationWrapper(event);
}
public onChangeValidation = (event: any) => {
const { onChange, validateon, validate } = this.props;
if (_.isFunction(onChange)) { onChange(event); }
if (validateon === "onChange" && !!validate) { this.validationWrapper(event); };
}
public callParentValidationErrorMethod = (hasError: boolean) => {
if(_.isFunction(this.props.validationerror)) {
this.props.validationerror(hasError);
}
}
public render() {
const { validationErrorMessage, validationError } = this.state
return (<TextField
{...this.props}
onBlur={(event: any) => { this.onBlurValidation(event); }}
onChange={(event: any) => { this.onChangeValidation(event); }
}
error={validationError}
helperText={validationErrorMessage}
/>)
}
}
export default ValidatedTextField;
Additional info: Not seen in IE only chrome so far and currently React v16.6
Alright Solved
Issue was that I was spreading all properties on the textfield in the component including the non existent attributes on the textfield.
Extrapolating the properties I did not need and only binding the default text field properties fixed this issue
const {validationerror,validate,validateon, ...textFieldProps } = this.props;
return (<TextField
{...textFieldProps}
onBlur={(event: any) => { this.onBlurValidation(event); }}
onChange={(event: any) => { this.onChangeValidation(event); }
}
error={validationError}
helperText={validationErrorMessage}
/>)

React controlled input cursor jumps

I am using React and have formatted a controlled input field, which works fine when I write some numbers and click outside the input field. However, when I want to edit the input, the cursor jumps to the front of the value in the input field. This only occur in IE, and not in e.g. Chrome. I've seen that for some programmers the cursor jumps to the back of the value. So I think the reason that my cursor is jumping to the front is because the value is aligned to the right instead of to the left in the input field. Here is a senario:
My first input is 1000
Then I want to edit it to 10003, but the result is
31000
Is there a way to controll that the cursor should not jump?
Here's a drop-in replacement for the <input/> tag. It's a simple functional component that uses hooks to preserve and restore the cursor position:
import React, { useEffect, useRef, useState } from 'react';
const ControlledInput = (props) => {
const { value, onChange, ...rest } = props;
const [cursor, setCursor] = useState(null);
const ref = useRef(null);
useEffect(() => {
const input = ref.current;
if (input) input.setSelectionRange(cursor, cursor);
}, [ref, cursor, value]);
const handleChange = (e) => {
setCursor(e.target.selectionStart);
onChange && onChange(e);
};
return <input ref={ref} value={value} onChange={handleChange} {...rest} />;
};
export default ControlledInput;
Taking a guess by your question, your code most likely looks similar to this:
<input
autoFocus="autofocus"
type="text"
value={this.state.value}
onChange={(e) => this.setState({value: e.target.value})}
/>
This may vary in behaviour if your event is handled with onBlur but essentially its the same issue. The behaviour here, which many have stated as a React "bug", is actually expected behaviour.
Your input control's value is not an initial value of the control when its loaded, but rather an underlying value bound to this.state. And when the state changes the control is re-rendered by React.
Essentially this means that the control is recreated by React and populated by the state's value. The problem is that it has no way of knowing what the cursor position was before it was recreated.
One way of solving this which I found to work is remembering the cursor position before it was re-rendered as follows:
<input
autoFocus="autofocus"
type="text"
value={ this.state.value }
onChange={(e) => {
this.cursor = e.target.selectionStart;
this.setState({value: e.target.value});
}
}
onFocus={(e) => {
e.target.selectionStart = this.cursor;
}
}
/>
This is my solution:
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.state = {
name: ""
};
//get reference for input
this.nameRef = React.createRef();
//Setup cursor position for input
this.cursor;
}
componentDidUpdate() {
this._setCursorPositions();
}
_setCursorPositions = () => {
//reset the cursor position for input
this.nameRef.current.selectionStart = this.cursor;
this.nameRef.current.selectionEnd = this.cursor;
};
handleInputChange = (key, val) => {
this.setState({
[key]: val
});
};
render() {
return (
<div className="content">
<div className="form-group col-md-3">
<label htmlFor="name">Name</label>
<input
ref={this.nameRef}
type="text"
autoComplete="off"
className="form-control"
id="name"
value={this.state.name}
onChange={event => {
this.cursor = event.target.selectionStart;
this.handleInputChange("name", event.currentTarget.value);
}}
/>
</div>
</div>
);
}
}
export default App;
This is an easy solution. Worked for me.
<Input
ref={input=>input && (input.input.selectionStart=input.input.selectionEnd=this.cursor)}
value={this.state.inputtext}
onChange={(e)=>{
this.cursor = e.target.selectionStart;
this.setState({inputtext: e.target.value})
/>
Explanation:
What we are doing here is we save the cursor position in onChange(), now when the tag re-renders due to a change in the state value, the ref code is executed, and inside the ref code we restore out cursor position.
If you're using textarea, then here's the hook based on Daniel Loiterton's code using TypeScript:
interface IControlledTextArea {
value: string
onChange: ChangeEventHandler<HTMLTextAreaElement> | undefined
[x: string]: any
}
const ControlledTextArea = ({ value, onChange, ...rest }: IControlledTextArea) => {
const [cursor, setCursor] = useState(0)
const ref = useRef(null)
useEffect(() => {
const input: any = ref.current
if (input) {
input.setSelectionRange(cursor, cursor)
}
}, [ref, cursor, value])
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setCursor(e.target.selectionStart)
onChange && onChange(e)
}
return <textarea ref={ref} value={value} onChange={handleChange} {...rest} />
}
As (I think) others have mentioned, React will keep track of this if you make your changes synchronously. Unfortunately, that's not always feasible. The other solutions suggest tracking the cursor position independently, but this will not work for input types like 'email' which will not allow you to use cursor properties/methods like selectionStart, setSelectionRange or whatever.
Instead, I did something like this:
const Input = (props) => {
const { onChange: _onChange, value } = props;
const [localValue, setLocalValue] = useState(value);
const onChange = useCallback(
e => {
setLocalValue(e.target.value);
_onChange(e.target.value);
},
[_onChange]
);
useEffect(() => {
setLocalValue(value);
}, [value]);
// Use JSX here if you prefer
return react.createElement('input', {
...props,
value: localValue,
onChange
});
};
This allows you to delegate the cursor positioning back to React, but make your async changes.
My cursor jumped always to the end of the line. This solution seems to fix the problem (from github):
import * as React from "react";
import * as ReactDOM from "react-dom";
class App extends React.Component<{}, { text: string }> {
private textarea: React.RefObject<HTMLTextAreaElement>;
constructor(props) {
super(props);
this.state = { text: "" };
this.textarea = React.createRef();
}
handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
const cursor = e.target.selectionStart;
this.setState({ text: e.target.value }, () => {
if (this.textarea.current != null)
this.textarea.current.selectionEnd = cursor;
});
}
render() {
return (
<textarea
ref={this.textarea}
value={this.state.text}
onChange={this.handleChange.bind(this)}
/>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
Here is my solution
const Input = () => {
const [val, setVal] = useState('');
const inputEl = useRef(null);
const handleInputChange = e => {
const { value, selectionEnd } = e.target;
const rightCharsCount = value.length - selectionEnd;
const formattedValue = parseInt(value.replace(/\D/g, ''), 10).toLocaleString();
const newPosition = formattedValue.length - rightCharsCount;
setVal(formattedValue);
setTimeout(() => {
inputEl.current.setSelectionRange(newPosition, newPosition);
}, 0);
};
return <input ref={inputEl} value={val} onChange={handleInputChange} />;
};
// Here is a custom hook to overcome this problem:
import { useRef, useCallback, useLayoutEffect } from 'react'
/**
* This hook overcomes this issue {#link https://github.com/reduxjs/react-redux/issues/525}
* This is not an ideal solution. We need to figure out why the places where this hook is being used
* the controlled InputText fields are losing their cursor position when being remounted to the DOM
* #param {Function} callback - the onChangeCallback for the inputRef
* #returns {Function} - the newCallback that fixes the cursor position from being reset
*/
const useControlledInputOnChangeCursorFix = callback => {
const inputCursor = useRef(0)
const inputRef = useRef(null)
const newCallback = useCallback(
e => {
inputCursor.current = e.target.selectionStart
if (e.target.type === 'text') {
inputRef.current = e.target
}
callback(e)
},
[callback],
)
useLayoutEffect(() => {
if (inputRef.current) {
inputRef.current.setSelectionRange(inputCursor.current, inputCursor.current)
}
})
return newCallback
}
export default useControlledInputOnChangeCursorFix
// Usage:
import React, { useReducer, useCallback } from 'react'
import useControlledInputOnChangeCursorFix from '../path/to/hookFolder/useControlledInputOnChangeCursorFix'
// Mimics this.setState for a class Component
const setStateReducer = (state, action) => ({ ...state, ...action })
const initialState = { street: '', address: '' }
const SomeComponent = props => {
const [state, setState] = useReducer(setStateReducer, initialState)
const handleOnChange = useControlledInputOnChangeCursorFix(
useCallback(({ target: { name, value } }) => {
setState({ [name]: value })
}, []),
)
const { street, address } = state
return (
<form>
<input name='street' value={street} onChange={handleOnChange} />
<input name='address' value={address} onChange={handleOnChange} />
</form>
)
}
For anybody having this issue in react-native-web here is solution written in TypeScript
const CursorFixTextInput = React.forwardRef((props: TextInputProps, refInput: ForwardedRef<TextInput>) => {
if(typeof refInput === "function") {
console.warn("CursorFixTextInput needs a MutableRefObject as reference to work!");
return <TextInput key={"invalid-ref"} {...props} />;
}
if(!("HTMLInputElement" in self)) {
return <TextInput key={"no-web"} {...props} />;
}
const { value, onChange, ...restProps } = props;
const defaultRefObject = useRef<TextInput>(null);
const refObject: RefObject<TextInput> = refInput || defaultRefObject;
const [ selection, setSelection ] = useState<SelectionState>(kInitialSelectionState);
useEffect(() => {
if(refObject.current instanceof HTMLInputElement) {
refObject.current.setSelectionRange(selection.start, selection.end);
}
}, [ refObject, selection, value ]);
return (
<TextInput
ref={refObject}
value={value}
onChange={event => {
const eventTarget = event.target as any;
if(eventTarget instanceof HTMLInputElement) {
setSelection({
start: eventTarget.selectionStart,
end: eventTarget.selectionEnd
});
}
if(onChange) {
onChange(event);
}
}}
{...restProps}
/>
)
});
The simplest and safest way of doing this is probably to save the cursor position before React renders the input and then setting it again after React finishes rendering.
import React, {ReactElement, useEffect, useRef} from "react";
/**
* Text input that preserves cursor position during rendering.
*
* This will not preserve a selection.
*/
function TextInputWithStableCursor(
props: React.InputHTMLAttributes<HTMLInputElement> & {type?: "text"}
): ReactElement {
const inputRef = useRef<HTMLInputElement>(null);
// Save the cursor position before rendering
const cursorPosition = inputRef.current?.selectionStart;
// Set it to the same value after rendering
useEffect(function () {
if (
typeof cursorPosition === "number" &&
document.activeElement === inputRef.current
) {
inputRef.current?.setSelectionRange(cursorPosition, cursorPosition);
}
});
return <input ref={inputRef} {...props} />;
}
If you faced an issue with the cursor jumping to the end after updating the input state and updating the cursor using refs -> I found a workaround for it by setting the cursor in Promise.resolve's microtask.
<input
value={value}
onChange={handleValueUpdate}
ref={inputRef}
/>
const handleValueUpdate = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
// ...
// some value handling logic
setValue(newValue)
const cursorPosition = getCursorPositionLogic();
/**
* HACK: set the cursor on the next tick to make sure that the value is updated
* useTimeout with 0ms provides issues when two keys are pressed same time
*/
Promise.resolve().then(() => {
inputRef.current?.setSelectionRange(cursorPosition, cursorPosition);
});
}
I know the OP is 5 years old but some are still facing the same kind of issue and this page has an high visibility on Google search.
Try by replacing :
<input value={...}
with
<input defaultValue={...}
This will solve most of the cases i've seen around there.
I tried all of the above solutions and none of them worked for me. Instead I updated both the e.currentTarget.selectionStart & e.currentTarget.selectionEnd on the onKeyUp React synthetic event type. For example:
const [cursorState, updateCursorState] = useState({});
const [formState, updateFormState] = useState({ "email": "" });
const handleOnChange = (e) => {
// Update your state & cursor state in your onChange handler
updateCursorState(e.target.selectionStart);
updateFormState(e.target.value);
}
<input
name="email"
value={formState.email}
onChange={(e) => handleOnChange(e)}
onKeyUp={(e) => {
// You only need to update your select position in the onKeyUp handler:
e.currentTarget.selectionStart = cursorState.cursorPosition;
e.currentTarget.selectionEnd = cursorState.cursorPosition;
}}
/>
Also, be aware that selectionStart & selectionEnd getters are not available on input fields of type email.

Resources