Using React.memo with hooks for controlled inputs - javascript

I'm providing some form functionality from a custom React hook. This hook has some functionality similar to Formik (but this is really basic stuff).
function useFormValidation(initialState, validate) {
const [values, setValues] = React.useState(initialState);
const [errors, setErrors] = React.useState({});
const [isSubmitting, setSubmitting] = React.useState(false);
React.useEffect(() => {
if (isSubmitting) {
const noErrors = Object.keys(errors).length === 0;
if (noErrors) {
console.log("authenticated!", values.email, values.password);
setSubmitting(false);
} else {
setSubmitting(false);
}
}
}, [errors]);
function handleChange(event) {
setValues({
...values,
[event.target.name]: event.target.value
});
}
function handleBlur() {
const validationErrors = validate(values);
setErrors(validationErrors);
}
function handleSubmit(event) {
event.preventDefault();
const validationErrors = validate(values);
setErrors(validationErrors);
setSubmitting(true);
}
return {
handleSubmit,
handleChange,
handleBlur,
values,
errors,
isSubmitting
};
}
The form is the following:
function Register() {
const {
handleSubmit,
handleChange,
handleBlur,
values,
errors,
isSubmitting
} = useFormValidation(INITIAL_STATE, validateAuth);
// const [email, setEmail] = React.useState("");
// const [password, setPassword] = React.useState("");
return (
<div className="container">
<h1>Register Here</h1>
<form onSubmit={handleSubmit}>
<Input
handleChange={handleChange}
handleBlur={handleBlur}
name="email"
value={values.email}
className={errors.email && "error-input"}
autoComplete="off"
placeholder="Your email address"
/>
{errors.email && <p className="error-text">{errors.email}</p>}
<Input
handleChange={handleChange}
handleBlur={handleBlur}
value={values.password}
className={errors.password && "error-input"}
name="password"
// type="password"
placeholder="Choose a safe password"
/>
{errors.password && <p className="error-text">{errors.password}</p>}
<div>
<button disabled={isSubmitting} type="submit">
Submit
</button>
</div>
</form>
</div>
);
}
And the memoized component is the next:
function Input({
handleChange,
handleBlur,
name,
value,
className,
autoComplete,
placeholder,
type
}) {
return (
<input
onChange={handleChange}
onBlur={handleBlur}
name={name}
value={value}
className={className}
autoComplete={autoComplete}
placeholder={placeholder}
type={type}
/>
);
}
function areEqual(prevProps, nextProps) {
console.log(`
prevProps: ${JSON.stringify(prevProps.value)}
nextProps: ${JSON.stringify(nextProps.value)}
`);
return prevProps.value === nextProps.value;
}
const useMemo = (component, propsAreEqual) => {
return memo(component, propsAreEqual);
};
export default useMemo(Input, areEqual);
I enter some text into the first input. Then, when I switch to the second Input and start typing, the first input loses the value. It's like the form is not rendering the LAST MEMOIZED input, but prior versions instead.
I'm a React beginner and can't figure out the solution. Any help please?

Try using the updater form of setState which takes a function:
function handleChange(event) {
// event.target wont be available when fn is run in setState
// so we save them in our own local variables here
const { name, value } = event.target;
setValues(prev => ({
...prev,
[name]: value
}));
}

Your areEqual method translates to
Re-render my Input ONLY when the value changes.
But in reality, your handleChange function from the hook is also changing. Also, you use the same handleChange for both the inputs. So, the Input "remembers" only the handleChange from the last time value had changed and since handleChange is tracking values via closure, it in-turn "remembers" the values when it was created.
Changing your areEqual method (or completely omitting it) to verify a change in handleChange, will solve your problem.
function areEqual(prevProps, nextProps) {
return (
prevProps.value === nextProps.value &&
prevProps.handleChange === nextProps.handleChange
);
}
A codesandbox of the solution here

Related

When to use react useState? [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed last year.
I have a simple registration form with 3 fields. I have stored the state in formValues with value & error associated with each field. Now when i submit the form without filling any or at least one field the form should be invalid but instead it shows validation messages with invalid fields but makes form valid. Even if i have added setTimeout the updated state is not available in the same handleSubmit. If i submit again the process works just fine. I understand that the state updation is async but if we see the logs in console the form's validation message is logged after formValues log in the render and those logs show that the state was updated correctly but the final validation message shows invalid state. If i change it to class component it works. Here's a link to codesandbox.
import React, { useState } from "react";
import { Button, Form, Col } from "react-bootstrap";
const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeout));
const RegistrationForm = () => {
const [formValues, setFormValues] = useState({
name: { value: "", error: null },
email: { value: "", error: null },
password: { value: "", error: null }
});
const handleInputChange = (e, field) => {
const { value } = e.target;
setFormValues(prevValues => ({
...prevValues,
[field]: { value, error: null }
}));
};
const validateForm = () => {
let updatedFormValues = { ...formValues };
Object.keys(formValues).forEach(field => {
if (!formValues[field].value) {
updatedFormValues = {
...updatedFormValues,
[field]: { ...updatedFormValues[field], error: "required" }
};
}
});
setFormValues(updatedFormValues);
};
const isFormValid = () =>
Object.keys(formValues).every(field => formValues[field].error === null);
const handleSubmit = async e => {
e.preventDefault();
validateForm();
await sleep(100);
if (!isFormValid()) {
console.log("form is not valid", formValues);
return;
}
console.log("form is valid", formValues);
// make api call to complete registration
};
console.log({ formValues });
return (
<Form className="registration-form" onSubmit={handleSubmit}>
<Form.Row>
<Col>
<Form.Group controlId="name">
<Form.Label>Name</Form.Label>
<Form.Control
type="text"
placeholder="Enter name"
value={formValues.name.value}
onChange={e => handleInputChange(e, "name")}
/>
<Form.Control.Feedback type="invalid" className="d-block">
{formValues.name.error}
</Form.Control.Feedback>
</Form.Group>
</Col>
<Col>
<Form.Group controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
placeholder="Enter email"
value={formValues.email.value}
onChange={e => handleInputChange(e, "email")}
/>
<Form.Control.Feedback type="invalid" className="d-block">
{formValues.email.error}
</Form.Control.Feedback>
</Form.Group>
</Col>
</Form.Row>
<Form.Row>
<Col>
<Form.Group controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
placeholder="Enter password"
value={formValues.password.value}
onChange={e => handleInputChange(e, "password")}
/>
<Form.Control.Feedback type="invalid" className="d-block">
{formValues.password.error}
</Form.Control.Feedback>
</Form.Group>
</Col>
<Col />
</Form.Row>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
);
};
export default RegistrationForm;
State updates are not just async but are als affected by closures in functional components, so using a sleep or timeout isn't going to leave your with an updated value in the same render cycle
You can read more about it in this post:
useState set method not reflecting change immediately
However, one solution in your case is to maintain a ref and toggle is value to trigger a useEffect in which you will validate the form post handleSubmit handler validates it and sets the formValues
Relevant code:
const validateFormField = useRef(false);
const handleInputChange = (e, field) => {
const { value } = e.target;
setFormValues(prevValues => ({
...prevValues,
[field]: { value, error: null }
}));
};
const validateForm = () => {
let updatedFormValues = { ...formValues };
Object.keys(formValues).forEach(field => {
if (!formValues[field].value) {
updatedFormValues = {
...updatedFormValues,
[field]: { ...updatedFormValues[field], error: "required" }
};
}
});
setFormValues(updatedFormValues);
validateFormField.current = !validateFormField.current;
};
const isFormValid = () =>
Object.keys(formValues).every(field => formValues[field].error === null);
const handleSubmit = async e => {
e.preventDefault();
validateForm();
// make api call to complete registratin
};
useEffect(() => {
if (!isFormValid()) {
console.log("form is not valid", formValues);
} else {
console.log("form is valid", formValues);
}
}, [validateFormField.current]); // This is fine since we know setFormValues will trigger a re-render
Working demo

onKeyDown get value from e.target.value?

What's wrong with my way to handle input in react? I want to detect keycode and prevent them to be entered into the input, but now below code doesn't seem working.
const Input = ({ placeholder }) => { const [inputValue, setInputValue] = useState("");
const handleKeyDown = e => {
console.log(e.target.value);
if ([188].includes(e.keyCode)) {
console.log("comma");
} else {
setInputValue(e.target.value);
} };
return (
<div>
<input
type="text"
value={inputValue}
onKeyDown={handleKeyDown}
placeholder={placeholder}
/>
</div> ); };
https://codesandbox.io/s/ancient-waterfall-43not?file=/src/App.js
you need to call e.preventDefault(), but also you need to add onChange handler to input:
const handleKeyDown = e => {
console.log(e.key);
if ([188].includes(e.keyCode)) {
console.log("comma");
e.preventDefault();
}
};
const handleChange = e => setInputValue(e.target.value);
...
<input
type="text"
value={inputValue}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
/>
When you update the value of input, you should use onChange().
But, If you want to catch some character and treat about that, you should use onKeyDown().
So, in your case, you should use both.
this is an example code about backspace.
const Input = (props) =>{
const [value, setValue] = React.useState('');
function handleChange(e){
setValue(e.target.value);
}
function handleBackSpace(e){
if(e.keyCode === 8){
//Do something.
}
}
return (
<div>
<input onChange={handleChange} onKeyDown={handleBackSpace} value={value} type="text" />
</div>
)
}
```
Pass the value from Parent to child. Please check the below code.
import React, { useState } from "react";
import "./styles.css";
const Input = ({ placeholder, inputValue, handleKeyDown }) => {
return (
<div>
<input
type="text"
value={inputValue}
onKeyDown={handleKeyDown}
placeholder={placeholder}
/>
</div>
);
};
export default function App() {
const [inputValue, setInputValue] = useState();
const handleKeyDown = e => {
console.log(e.keyCode);
console.log(e.target.value);
if ([40].includes(e.keyCode)) {
console.log("comma");
} else {
setInputValue(e.target.value);
}
};
return (
<div className="App">
<Input
placeholder="xx"
value={inputValue}
handleKeyDown={handleKeyDown}
/>
</div>
);
}
onKeyDown, onKeyUp, and onKeyPress contain the old value of the target element.
onInput event gets the new value of the target element.
check the below link I add some console log. which help you to understand which event contains the value
https://codesandbox.io/s/ecstatic-framework-c4hkw?file=/src/App.js
I think you should not use the onKeyDown event on this case to filter your input. The reason is that someone could simply copy and paste the content into the input. So it would not filter the comma character.
You should use the onChange event and add a Regex to test if the input is valid.
const Input = ({ placeholder }) => {
const [inputValue, setInputValue] = useState("");
const handleChange = e => {
const filteredInput = e.target.value.replace(/[^\w\s]/gi, "");
setInputValue(filteredInput);
};
return (
<div>
<input
type="text"
value={inputValue}
onChange={handleChange}
placeholder={placeholder}
/>
</div>
);
};
But how it works...
So the regex is currently allowing any word, digit (alphanumeric) and whitespaces. You could for example extend the whitelist to allow # by doing const filteredInput = e.target.value.replace(/[^\w\s#]/gi, ""); Any rule inside the [^] is allowed. You can do some regex testing here https://regexr.com/55rke
Also you can test my example at: https://codesandbox.io/s/nice-paper-40dou?file=/src/App.js

useState with react giving me issues with onChange handler for google recaptcha

I am setting a state variable with useState whether or not a recaptcha has been clicked, but when I use the setState function my captcha variable doesn't pass ot my on submit, don't understand why, If I remove the setState the captcha variable passes just fine to my onSubmit.
If I remove setSubmitButton(false) everything works fine, not quite sure why.
When I run the setSubmittButton(false) captcha is endefined in my submit function when I dont have it there I get the correct captcha value in my submit function.
import React, { useState } from "react"
import { useForm } from "react-hook-form"
import ReCAPTCHA from "react-google-recaptcha"
const ContactForm = () => {
const { register, handleSubmit, watch, errors } = useForm()
const isBrowser = typeof window !== `undefined`
let location
if (isBrowser) {
location = window.location.hostname
}
let fetchUrl
if (location === "localhost") {
fetchUrl = `http://localhost:8888/.netlify/functions/contact`
} else if (location === "fsdf.gtsb.io") {
fetchUrl = `https://fdsfd/.netlify/functions/contact`
} else {
fetchUrl = "/.netlify/functions/contact"
}
console.log(fetchUrl)
const onSubmit = async data => {
setLoading(true)
console.log(captcha, "captcha value final")
const response = await fetch(fetchUrl, {
method: "POST",
body: JSON.stringify({ data, captcha: captcha }),
})
.then(response => response.json())
.then(data => {
console.log(data)
if (data.captcha === false) {
setCaptchaFailed(true)
}
})
.catch(error => {
console.error("Error:", error)
})
}
const [submitButton, setSubmitButton] = useState(true)
const [loading, setLoading] = useState(false)
const [captchaFailed, setCaptchaFailed] = useState(false)
let captcha
function onChange(value) {
setSubmitButton(false) // IF I REMOVE THIS LINE EVERYTHING WORKS FINE ******
console.log("Captcha value:", value)
captcha = value
}
function error(value) {
alert(value)
}
return (
<>
{/* "handleSubmit" will validate your inputs before invoking "onSubmit" */}
{!loading ? (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label for="name">Name</label>
<input name="name" ref={register({ required: true })} />
{errors.name && <span>This field is required</span>}
</div>
<div>
<label for="email">Email</label>
<input
type="email"
name="email"
ref={register({ required: true })}
/>
{errors.email && <span>This field is required</span>}
</div>
<div>
<label for="message">Message</label>
<textarea name="message" ref={register({ required: true })} />
{errors.message && <span>This field is required</span>}
</div>
<ReCAPTCHA
sitekey="fdsfsa"
onChange={onChange}
onErrored={error}
/>
<input
type="submit"
className={submitButton ? "disabled" : ""}
disabled={submitButton ? "disabled" : ""}
/>
</form>
) : (
<>
{captchaFailed ? (
<>
<p>Captcha Verification Failed</p>
</>
) : (
<h1>Loading</h1>
)}
</>
)}
</>
)
}
export default ContactForm
You should store the captcha value in a state variable (or ref) instead of a plain JS variable. The plain JS variable will reset to undefined when the component re-renders (after the setSubmitButton changes the state).
const [ captchaValue, setCaptchaValue ] = useState(null);
function onChange(value) {
setSubmitButton(false);
console.log("Captcha value:", value);
setCaptchaValue(value);
}
const onSubmit = async data => {
setLoading(true)
console.log(captchaValue, "captcha value final");
...
}
Tip: Also, make sure to replace all the occurrences of the for html attributes to htmlFor. You can use an HTML-to-JSX transformer for automating such tasks.

Property 'reset' does not exist on type 'EventTarget'.ts(2339)

I came across a SO overflow which suggested that we can use e.target.reset()on forms to reset forms after submitting. However, I am unable to use it in my case:
export default function RemoveUserPage() {
const [isSubmitted, setIsSubmitted] = useState(false);
const [isRemoved, setIsRemoved] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [removeUser] = useMutation<DeleteUserReponse>(REMOVE_USER);
let submitForm = (email: string) => {
setIsSubmitted(true);
removeUser({
variables: {
email: email,
},
})
.then(({ data }: ExecutionResult<DeleteUserReponse>) => {
setIsRemoved(true);
}})
};
const initialSTATE={ email: '' }
return (
<div>
<Formik
//initialValues={{ email: '' }}
initialValues={{ ...initialSTATE}}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
}, 1000);
}}
validationSchema={schema}>
{props => {
const {
values: { email },
errors,
touched,
handleChange,
isValid,
setFieldTouched,
} = props;
const change = (name: string, e: FormEvent) => {
e.persist();
handleChange(e);
setFieldTouched(name, true, false);
};
return (
<div className="main-content">
<form
style={{ width: '100%' }}
onSubmit={e => {
e.preventDefault();
submitForm(email);
e.target.reset();
}}>
<div>
<TextField
variant="outlined"
margin="normal"
id="email"
name="email"
helperText={touched.email ? errors.email : ''}
error={touched.email && Boolean(errors.email)}
label="Email"
value={email}
onChange={change.bind(null, 'email')}
/>
<br></br>
<CustomButton
disabled={!isValid || !email}
text={'Remove User'}
/>
</div>
</form>
<br></br>
{isSubmitted && StatusMessage(isRemoved, errorMessage)}
</div>
);
}}
</Formik>
</div>
);
}
If I use it at the end of my onSubmit, I get the error mentioned above. How can I fix this?
I had the same problem, since I know that the e.target is a form element I just set it's type like so:
const target = e.target as HTMLFormElement
target.reset()
It works. I hope this helps you.
I came across a SO overflow which suggested that we can use e.target.reset() on forms to reset forms after submitting.
Could be valid before react era. Nowadays react updates real DOM on props changes.
Want to reset form? Render it using new values!
Based on docs you should pass handleSubmit prop to rendered <form/>:
<form onSubmit={props.handleSubmit}>
This handleSubmit will call (after validation) Formik's onSubmit handler. This is a place where you should define custom logic.
onSubmit is called with (values, actions) so you can
submitForm(values.email);
actions.resetForm()
or pass actions to your submitForm and use resetForm in this method.

Redux-form's Field component is not triggering normalize function if Field's type is number

I'm passing antd's component (FormFieldInput) to redux-form's Field component. Everything works well with "text" and "telephone" input types. Normalize number function stops working as soon as I change Field's type to "number".
I can see that FormFieldInput component is triggered only when I input numbers. When I'm typing alphabetic characters into the input FormFieldInput console log at the top of the function is not returning new values.
Normalizer:
const normalizeNumber = (value /* , previousValue */) => {
console.log('---input', value);
if (!value) {
return value;
}
const onlyNums = value.replace(/[^\d]/g, '');
console.log('---output', onlyNums);
return onlyNums;
};
Usage:
<Field
name="size"
type="number"
component={FormFieldInput}
label="Size"
placeholder="Size"
required
validate={[required, maxLength255]}
normalize={normalizeNumber}
/>
FormFieldInput:
const FormFieldInput = ({
input,
label,
type,
placeholder,
required,
meta: { touched, error, warning }
}) => {
const [hasError, setHasError] = useState(false);
const [hasWarning, setHasWarning] = useState(false);
console.log('---input', input);
useEffect(() => {
setHasError(!!error);
}, [error]);
useEffect(() => {
setHasWarning(!!warning);
}, [warning]);
const ref = createRef();
return (
<div className="form-item">
<div className="form-item__label">
{`${label}:`}
{required && <span style={{ color: 'red' }}> *</span>}
</div>
<div className={`form-item__input`}>
<AntInput
{...input}
ref={ref}
placeholder={placeholder}
type={type}
/>
</div>
</div>
);
};
export const AntInput = React.forwardRef((props, ref) => {
const { placeholder, type, suffix } = props;
console.log('---type', type);
return <Input {...props} ref={ref} placeholder={placeholder} type={type} suffix={suffix} />;
});
I expect all input data to go through normalize function, but somehow alphabetic characters are going through it.

Categories

Resources