React form validation hook causes infinite re-render loop - reactjs

I've checked for other questions, they didn't help me
I built a React hook to validate my form inputs. However, it causes an infinite re-render loop. I've tracked down the problem to the useEffect dependency array. When I exclude the validators dependency, it works great! I won't ever change the validators during runtime, so is this prop is not needed in the dependency array? My ESLint react-hooks-plugin keeps giving me a warning that the validators dependency is missing. Please help me out. Can I leave out the validators dependency from the useEffect dependency array if I won't change it during runtime? Here is my hook and my form component:
import { useState, useEffect } from "react";
function setObjectValues<
K extends { [key: string]: unknown },
T
>(
object: K,
value: T
): { [key in keyof K]?: T } {
const initialResults: {
[key in keyof K]?: T;
} = {};
for (const key in object) {
initialResults[key] = value;
}
return initialResults;
}
export function useValidation<
K,
T extends {
[key: string]: (value: K) => boolean;
}
>(
value: K,
validators: T
): {
valid: boolean;
results: { [key in keyof T]?: boolean };
} {
const [results, setResults] = useState<
{ [key in keyof T]?: boolean }
>(setObjectValues(validators, true));
useEffect(() => {
const newResults: {
[key in keyof T]?: boolean;
} = {};
for (const key in validators) {
const valid = validators[key](value);
newResults[key] = valid;
}
setResults(newResults);
}, [value, validators]);
const valid = Object.values(results).every(
(item) => item === true
);
return { valid, results };
}
My component:
import { NextPage } from "next";
import {
useFirebase,
useValidation,
} from "app/hooks";
import {
useState,
useCallback,
FormEvent,
} from "react";
import { useRouter } from "next/router";
type InputType = "email" | "password";
const SignUp: NextPage = () => {
const firebase = useFirebase();
const router = useRouter();
const [email, setEmail] = useState("");
const {
valid: emailValid,
results: emailValidationResults,
} = useValidation(email, {
containsAt: (value) => value.includes("#"),
});
const [password, setPassword] = useState("");
const {
valid: passwordValid,
results: passwordValidationResults,
} = useValidation(password, {
isLongEnough: (value) => value.length >= 8,
containsLowerCase: (value) =>
value.toUpperCase() !== value,
containsUpperCase: (value) =>
value.toLowerCase() !== value,
containsNumber: (value) => /\d/.test(value),
});
const handleSubmit = useCallback(
(event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (
emailValid === true &&
passwordValid === true &&
email !== "" &&
password !== ""
) {
const error = firebase.createUser(
email,
password
);
if (error) {
console.warn(error.code);
} else {
router.push("/");
}
} else {
console.warn("Invalid user values");
}
},
[
email,
emailValid,
firebase,
password,
passwordValid,
router,
]
);
console.log(emailValid, passwordValid);
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input
value={email}
onChange={(event): void =>
setEmail(event.target.value)
}
id="email"
placeholder="Email"
/>
<p>{emailValid}</p>
<label htmlFor="password">Password</label>
<input
value={password}
onChange={(event): void =>
setPassword(event.target.value)
}
id="password"
placeholder="Password"
/>
<p>{passwordValid}</p>
<button type="submit">Submit</button>
</form>
);
};
export default SignUp;

const {
valid: passwordValid,
results: passwordValidationResults,
} = useValidation(password, {
isLongEnough: (value) => value.length >= 8,
containsLowerCase: (value) =>
value.toUpperCase() !== value,
containsUpperCase: (value) =>
value.toLowerCase() !== value,
containsNumber: (value) => /\d/.test(value),
});
Here, validators is actually an Object that is created during Signup render. When you create a new Object, it is always a new, different object, even though the values inside of it might be the same. This is the reason why adding it to the dependencies array causes infinite re-renders.
If the object does not depend on your component's state or props, move the declaration to outside of the component so that it is only created once.
// outside SignUp
const validators = {
isLongEnough: (value) => value.length >= 8,
containsLowerCase: (value) =>
value.toUpperCase() !== value,
containsUpperCase: (value) =>
value.toLowerCase() !== value,
containsNumber: (value) => /\d/.test(value),
};
// inside SignUp
const {
valid: passwordValid,
results: passwordValidationResults,
} = useValidation(password, validators);
I would suggest keeping validators in the dependencies array, because it should still work anyway. Leaving it out most of the time is a code smell. Most of the time, if validators changed, you will want the effect to be re-run again.

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.

How to use and specify Generic Hooks in React with Typescript?

I'm trying to create a generic hooks to handle the button input elements, which return array of input value, bind object and reset handler.
Component
import React, { useState } from "react";
import { useInput } from "../services/FormInputHooks";
export type AddTransactionProps = {};
export const AddTransaction: React.FC<AddTransactionProps> = () => {
const [text, bindText, resetText] = useInput<string>("");
const [amount, bindAmount, resetAmount] = useInput<number>(0.0);
return (
<>
<h3>Add new transaction</h3>
<form>
<div className="form-control">
<label htmlFor="text">Text</label>
<input
type="text"
{...bindText}
placeholder="Enter text.."
/>
</div>
<div className="form-control">
<label htmlFor="amount">
Amount <br />
(negative - expense, positive - income)
</label>
<input
type="number"
{...bindAmount}
placeholder="Enter amount.."
/>
</div>
<button className="btn"> Add Transaction</button>
</form>
</>
);
};
export default AddTransaction;
Hook
import { useState } from "react";
export function useInput<T>(
initialValue: T
): [T, any, React.Dispatch<React.SetStateAction<T>>] {
const [value, setValue] = useState<T>(initialValue);
const reset = () => {
setValue(initialValue);
};
const bind = {
value,
onChange: e => {
setValue(e.target.value);
}
};
return [value, bind, reset];
}
Problem I'm Facing
Parameter 'e' implicitly has an 'any' type. TS7006
12 | const bind = {
13 | value,
> 14 | onChange: e => {
| ^
15 | setValue(e.target.value);
16 | }
17 | };
Though i've specified the type of any for the bind object, it shows the above error. I've even tried with following code to specify the return type.
[T, {T: onChange: (e: any) => void}, React.Dispatch<React.SetStateAction<T>>]
The problem is not the type you define for the hooks return value, it is that the bind object does not have any type annotation so its onChange method's e param will be implicitly any.
One possible sulution to fix its type annotation:
import { useState, ChangeEventHandler } from "react";
interface ResetFunction {
(): void
}
interface Bind<T> {
value: T,
onChange: ChangeEventHandler<any>
}
export function useInput<T>(
initialValue: T
): [T, Bind<T>, ResetFunction] {
const [value, setValue] = useState<T>(initialValue);
const reset = () => {
setValue(initialValue);
};
const bind: Bind<T> = {
value,
onChange: e => {
setValue(e.target.value);
}
};
return [value, bind, reset];
}
Typescipt playground
import React, { useState } from "react";
export function useInput<T>(
initialValue: T
): [T, any, React.Dispatch<React.SetStateAction<T>>] {
const [value, setValue] = useState<T>(initialValue);
const reset = () => {
setValue(initialValue);
};
const bind = {
value,
onChange: (e: React.ChangeEvent<any>) => {
setValue(e.target?.value);
}
};
return [value, bind, reset];
}
Playground

How to add types to React Table IndeterminateCheckbox method

I'm really a beginner at typescript world and, I'm currently using React Table library that has no types by default on documentation.
So, I would like to ask your help to add the types to IndeterminateCheckbox method.
const IndeterminateCheckbox = React.forwardRef(
({ indeterminate, ...rest }, ref) => {
const defaultRef = React.useRef()
const resolvedRef = ref || defaultRef
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate
}, [resolvedRef, indeterminate])
return (
<>
<input type="checkbox" ref={resolvedRef} {...rest} />
</>
)
}
)
Here is the link to sandbox from React Table docs:
https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/row-selection-and-pagination?from-embed=&file=/src/App.js:613-1010
My second question is: Where can I find the types and maybe add them by myself?
1. Create #types/react.d.ts
/* eslint-disable */
declare namespace React {
export default interface RefObject<T> {
current: T | null;
}
}
2. The input component
/* eslint-disable no-param-reassign */
import React, { forwardRef, useEffect, useRef } from 'react';
interface IIndeterminateInputProps {
indeterminate?: boolean;
name: string;
}
const useCombinedRefs = (
...refs: Array<React.Ref<HTMLInputElement> | React.MutableRefObject<null>>
): React.MutableRefObject<HTMLInputElement | null> => {
const targetRef = useRef(null);
useEffect(() => {
refs.forEach(
(ref: React.Ref<HTMLInputElement> | React.MutableRefObject<null>) => {
if (!ref) return;
if (typeof ref === 'function') {
ref(targetRef.current);
} else {
ref.current = targetRef.current;
}
},
);
}, [refs]);
return targetRef;
};
const IndeterminateCheckbox = forwardRef<
HTMLInputElement,
IIndeterminateInputProps
>(({ indeterminate, ...rest }, ref: React.Ref<HTMLInputElement>) => {
const defaultRef = useRef(null);
const combinedRef = useCombinedRefs(ref, defaultRef);
useEffect(() => {
if (combinedRef?.current) {
combinedRef.current.indeterminate = indeterminate ?? false;
}
}, [combinedRef, indeterminate]);
return (
<>
<input type="checkbox" ref={combinedRef} {...rest} />
</>
);
});
export default IndeterminateCheckbox;
I just want to put code here so it is visible to everyone having issues with it. Solution is from the thread from Megha's comment:
import React from "react";
type Props = {
indeterminate?: boolean;
};
const TableCheckBox: React.ForwardRefRenderFunction<HTMLInputElement, Props> = ({ indeterminate = false, ...rest }, ref) => {
const defaultRef = React.useRef<HTMLInputElement>();
const resolvedRef = (ref || defaultRef) as React.MutableRefObject<HTMLInputElement>;
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate;
}, [resolvedRef, indeterminate]);
return (
<>
<input type="checkbox" ref={resolvedRef} {...rest} />
</>
);
};
export default React.forwardRef(TableCheckBox);
Link: https://github.com/TanStack/table/discussions/1989#discussioncomment-4388612

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