I created an Input component like this:
import React, { useState } from 'react'
const Input = ({ name, placeholder, type, className, inputRef, defaultValue, preventDefaultBehavior, style, timeout = 2000,
onChange, onDoubleClick, onFocus, onBlur }) => {
const [value, setValue] = useState(defaultValue);
const [typingTimeout, setTypingTimeout] = useState(0);
function handleChange(e) {
if (typingTimeout)
clearTimeout(typingTimeout);
setValue(e.target.value);
if (onChange) {
setTypingTimeout(
setTimeout(function () {
onChange({ target: { name, value } });
}, timeout)
);
}
};
function handleKeyPress(e) {
if (e.keyCode !== 13 || timeout <= 1000)
return;
if (typingTimeout)
clearTimeout(typingTimeout);
onChange({ target: { name, value } });
}
return (
<input
name={name}
className={className}
type={type}
ref={inputRef}
placeholder={placeholder}
style={style}
value={value}
onKeyPress={handleKeyPress}
onChange={handleChange}
onDoubleClick={onDoubleClick}
onMouseDown={preventDefaultBehavior}
onBlur={onBlur}
onFocus={onFocus}
/>
);
};
export default Input;
I'm using that Input component on the parent like this:
const [url, setUrl] = useState('');
<Input onChange={e => setUrl(e.target.value)} name="url"
placeholder="URL" type="text" defaultValue={url} />
However, parent component is not taking the last character of the input, such as:
input: 'abc'
parent state: 'ab'
I tried useEffect and async/await but it didn't work for me.
Related
I have search component with validate js.
Problem: when my input in foucs first time, validate and request dont work, but when i lose focus my input, and click it again, and try again, search working without validation
interface IProps {
onSearchChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
const Search: React.FC<IProps> = ({ onSearchChange }) => {
const inputRef = useRef<HTMLInputElement>(null);
const [inputIsTouched, setInputIsTouched] = useState(false);
const currentValue = inputRef.current?.value && inputRef.current.value;
const validateErrors = validate({ currentValue }, constraints);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (validateErrors?.currentValue) {
return;
}
currentValue && onSearchChange && onSearchChange(event);
setInputIsTouched(true);
};
const debouncedOnChange = debounce(handleChange, 1000);
return (
<div className={classes['Root']}>
<Input
type="text"
autoComplete="off"
placeholder="..."
onChange={debouncedOnChange}
ref={inputRef}
onBlur={() => setInputIsTouched(true)}
isError={inputIsTouched && !!validateErrors?.currentValue}
/>
<div className={classes['ErrorContainer']}>
{inputIsTouched && validateErrors?.currentValue && (
<Text color="error" size="s">
{validateErrors.currentValue}
</Text>
)}
</div>
</div>
);
};
That's expected because on first render, currentValue is undefined (as inputRef.current is null) and there's nothing calling handleChange to trigger the search.
You need to make sure the handleChange logic also runs on the initial render, so it should look something like this:
const Search: React.FC<IProps> = ({ onSearchChange }) => {
// Use a single object for all input state props:
const [{
isTouched,
validateErrors,
}, setInputState] = useState({
isTouched: false,
validateErrors: null,
});
const inputRef = useRef<HTMLInputElement>(null);
// Debounce only search callback:
const debouncedSearchChange = debounce(onSearchChange, 1000);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
// Get the current value:
const currentValue = e.currentTarget.value;
// Validate it:
const validateErrors = validate({ currentValue }, constraints);
if (validateErrors?.currentValue) {
// And handle error:
setInputState(prevState => ({ ...prevState, validateErrors }));
return;
}
// Or success:
setInputState(prevState => ({ ...prevState, validateErrors: null }));
// And trigger the debounced search if needed:
if (currentValue && debouncedSearchChange ) debouncedSearchChange(event);
}, [constraints, debouncedSearchChange]);
// Trigger validation and search on first render:
useEffect(() => {
const inputElement = inputRef.current;
// TypeScript will complain about this line, so you might want to
// re-structure the logic above to accommodate this:
if (inputElement) handleChange({ currentTarget: inputElement });
}, []);
return (
<div className={classes['Root']}>
<Input
type="text"
autoComplete="off"
placeholder="..."
onChange={handleChange}
ref={inputRef}
onBlur={() => setInputState(prevState => ({ ...prevState, isTouched: true }))}
isError={inputIsTouched && !!validateErrors?.currentValue}
/>
<div className={classes['ErrorContainer']}>
{inputIsTouched && validateErrors?.currentValue && (
<Text color="error" size="s">
{validateErrors.currentValue}
</Text>
)}
</div>
</div>
);
};
I have a input component that does validation. In a reduced form it looks like this:
const InputField = ({validation, name}) => {
const [value, setValue] = useState("");
const [error, setError] = useState(false);
const handleChange = (e) => {
let errors = 0;
if (validation.includes("required") && val === "") {
errors++;
}
if (errors > 0) {
setError(true);
} else {
setError(false);
}
setValue(e.target.value)
}
return (
<input
type="text"
value={value}
onChange={handleChange}
name={name}
/>
);
};
If I am using this component multiple times in a parent like
const Parnet = () => {
// 🔥🔥🔥 help required here
const groupedErrorValueBool = true;
return (
<>
<InputField name="Name" validation="required" />
<InputField name="Email" validation="required" />
<InputField name="Birthday" validation="required" />
<button disabled={groupedErrorValueBool}>Submit</button>
</>
);
How can I get a grouped value of errors? Like: I want to disable a submit button, if any field has any error.
Ideally I know which component has an error, so that I can print meaningful comments.
Implementation
You could do it like this:
const InputField = ({validation, onErrorChanged}) => {
const [value, setValue] = React.useState("test");
const [error, setError] = React.useState(false);
const handleChange = (e) => {
let errors = 0;
if (validation.includes("required") && e.target.value === "") {
errors++;
}
if (errors > 0 && !error) {
// we have error(s) and the error state is set to false
setError(true);
onErrorChanged(true);
} else if (errors === 0 && error){
// we don't have any error but the error state is set to true
setError(false);
onErrorChanged(false);
}
setValue(e.target.value)
}
return (
<input
type="text"
value={value}
onChange={handleChange}
/>
);
};
const InputGroup = () => {
const [errors, setErrors] = React.useState(0);
function errorChanged(hasError){
if(hasError) setErrors(errors + 1);
else setErrors(errors - 1);
}
return (
<React.Fragment>
<InputField validation="required" onErrorChanged={errorChanged}/>
<InputField validation="required" onErrorChanged={errorChanged}/>
<InputField validation="required" onErrorChanged={errorChanged}/>
<button disabled={errors === 0 ? false: true}>Submit</button>
</React.Fragment>
);
}
ReactDOM.render(<InputGroup/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Explanation
Child component
You need to pass a callback function to the children that is called whenever the error state changes on a child element i. e.
when we have found an error in the input and error state is currently set to false
when we have no error(s) in the input and error state is currently set to true
Parent component
In the parent component we need a state errors which is a counter that counts the number of child component that have errors. Additionally we need to define the above mentioned callback function such that it increments the counter whenever a child component reports it has an error and decrements the counter when a child component reports it has switched to state no error.
If the counter equals zero we know we don't have any error in any of our child components, if it's not null we know how many child components have errors.
The groupedErrorValueBool state would need to be in your parent component then, then you give the state handler to the children
const InputField = ({ validation, setGroupErrorBool }) => {
const [value, setValue] = useState("");
const [error, setError] = useState(false);
const handleChange = (e) => {
let errors = 0;
if (validation.includes("required") && val === "") {
errors++;
}
if (errors > 0) {
setError(true);
setGroupErrorBool(true);
} else {
setError(false);
}
setValue(e.target.value);
};
return <input type="text" value={value} onChange={handleChange} />;
};
const Parent = () => {
const [groupErrorBool, setGroupErrorBool] = useState(false);
return (
<>
<InputField validation="required" setGroupErrorBool={setGroupErrorBool} />
<InputField validation="required" setGroupErrorBool={setGroupErrorBool} />
<button disabled={groupErrorBool}>Submit</button>
</>
);
};
Form stuff is admittedly can be very difficult. That's why I use "Formik" for my React forms. Formik I believe will use a parent/overarching form control element with rendered props technique to expose form state handlers, which could also be something you can try yourself
I am using React with FluentUI to build a simple form, code is following
import React, { useState, FormEvent } from "react";
import { PrimaryButton } from "office-ui-fabric-react/lib/Button";
import { IUserFormValues } from "../../models/user";
import { Stack, TextField } from "#fluentui/react";
const NewUIForm = () => {
const [user, setUser] = useState<IUserFormValues>({
email: "",
password: "",
});
const handleInputChange = (
event: FormEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = event.currentTarget;
setUser({ ...user, [name]: value });
};
const log123 = () => {
console.log(user.email);
};
return (
<Stack>
<Stack>
<Stack.Item grow>
<TextField
placeholder="UserName"
label="User Name"
value={user.email}
name="email"
onChange={handleInputChange}
/>
<TextField
placeholder="Password"
label="Password"
value={user.password}
name="password"
onChange={handleInputChange}
/>
</Stack.Item>
<PrimaryButton onClick={log123}>Add</PrimaryButton>
</Stack>
</Stack>
);
};
export default NewUIForm;
every time when I type something in the TextField I will get this error
TypeError: Cannot destructure property 'name' of 'event.currentTarget' as it is null.
Can someone help me? Thanks!!
Fluent UI onChange function expects two parameters: event and value(optional)
(event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => void
https://developer.microsoft.com/en-us/fluentui#/controls/web/textfield#implementation
You may change your handleInputChange function like this:
const handleInputChange = (
event: { target: HTMLInputElement },
newValue:String
):void => {
const { name } = event.target;
setUser({ ...user, [name]: newValue });
};
You can check this working fiddle from here
I want to do a debounce for custom input, but my problem is I can't stop useEffect from trigger on initial render
import { useDebouncedCallback } from "use-debounce";
interface myInputProps {
getValue: any;
}
const MyInput = ({ getValue }: myInputProps) => {
const [value, setValue] = useState("");
React.useEffect(() => {
getValue(value);
}, [value]);
return (
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
);
};
export default function App() {
const [debouncedCallback] = useDebouncedCallback(value => {
console.log(value);
}, 1000);
return (
<div className="App">
<MyInput getValue={debouncedCallback} />
</div>
);
}
https://codesandbox.io/s/upbeat-lamport-ukq70?file=/src/App.tsx
I've also tried useLayoutEffect but it doesn't solve the problem.
We could use useRef to keep track of if it's the first time the useEffect hook is being run.
https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
Sandbox link: https://codesandbox.io/s/confident-cerf-flkf2?file=/src/App.tsx
const MyInput = ({ getValue }: myInputProps) => {
const [value, setValue] = useState("");
const first = useRef(true);
React.useEffect(() => {
if (first.current) {
first.current = false;
return;
}
getValue(value);
}, [value]);
return (
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
);
};
Set initial value to undefined and you can explicitly check for undefined. Once the user enter, it won't be undefined.
const MyInput = ({ getValue }: myInputProps) => {
const [value, setValue] = useState(undefined);
React.useEffect(() => {
if (value === undefined) {
return;
}
getValue(value);
}, [value]);
return (
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
);
};
I have a React Form app with name and description fields.
The form data is held in a local state object using Hooks:
const [data,setData] = useState({name: '', description: ''}).
The <Form /> element creates inputs and passes their value using <Field initialValue ={data.name} />
Within the <Field /> element, this initialValue is passed to the state, which controls the input value (updated onChange):
const [value,setValue] = useState(initialValue).
But if I reset the data object (see handleResetClick function), the inputs don't clear (even though the data object clears). What am I doing wrong? I thought that changing the data would cause a re-render and re-pass initialValue, resetting the input.
Codepen example here - when I type in the inputs, the data object updates, but when I click Clear, the inputs don't empty.
function Form() {
const [data, setData] = React.useState({name: '', description: ''});
React.useEffect(() => {
console.log(data);
},[data]);
const onSubmit = (e) => {
// not relevant to example
e.preventDefault();
return;
}
const handleResetClick = () => {
console.log('reset click');
setData({name: '', description: ''})
}
const onChange = (name, value) => {
const tmpData = data;
tmpData[name] = value;
setData({
...tmpData
});
}
return (
<form onSubmit={onSubmit}>
<Field onChange={onChange} initialValue={data.name} name="name" label="Name" />
<Field onChange={onChange} initialValue={data.description} name="description" label="Description" />
<button type="submit" className="button is-link">Submit</button>
<button onClick={handleResetClick} className="button is-link is-light">Clear</button>
</form>
)
}
function Field(props) {
const {name, label, initialValue, onChange} = props;
const [value, setValue] = React.useState(initialValue);
return (
<div>
<div className="field">
<label className="label">{label}</label>
<div className="control">
<input
name={name}
className="input"
type="text"
value={value}
onChange={e => {
setValue(e.target.value)
onChange(name, e.target.value)
}}
/>
</div>
</div>
</div>
)
}
class App extends React.Component {
render() {
return (
<div className="container">
<Form />
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
)
On handleResetClick you change the data state of Form, but it doesn't affect its children.
Try adding a listener for initialValue change with useEffect:
function Field(props) {
const { name, label, initialValue, onChange } = props;
const [value, setValue] = React.useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return ...
}
You may be better off having Field as a controlled component (ie it's state is managed by the parent component rather than maintaining its own state). In this example I've swapped in value instead of initialValue and simply passed that down as props to the field. onChange then calls the parent method and updates the state there (which is automatically passed back down to the field when it renders):
const { useState, useEffect } = React;
function Form() {
const [data, setData] = React.useState({
name: '',
description: ''
});
useEffect(() => {
console.log(data);
}, [data]);
const onSubmit = (e) => {
e.preventDefault();
return;
}
const handleResetClick = () => {
setData({name: '', description: ''})
}
const onChange = (e) => {
const { target: { name, value } } = e;
setData(data => ({ ...data, [name]: value }));
}
return (
<form onSubmit={onSubmit}>
<Field onChange={onChange} value={data.name} name="name" label="Name" />
<Field onChange={onChange} value={data.description} name="description" label="Description" />
<button type="submit" className="button is-link">Submit</button>
<button onClick={handleResetClick} className="button is-link is-light">Clear</button>
</form>
)
}
function Field(props) {
const {name, label, value, onChange} = props;
return (
<div>
<div className="field">
<label className="label">{label}</label>
<div className="control">
<input
name={name}
className="input"
type="text"
value={value}
onChange={onChange}
/>
</div>
</div>
</div>
)
}
function App() {
return (
<div className="container">
<Form />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>