useEffect inside customHook not happens sometimes (from unknown reason). Using useRef - reactjs

I have this custom hook which supposed to make debounce email validation.
Suddenly I notice that it's not happening in all types.
Sometimes in the first types it's happen sometimes not.
See the log of "useEffect" which won't happen (for me - in each case) with any type. And when it's happening it's taking the previous value.
the Custom hook:
export function useDebounceEmailValidation(value, delay) {
console.log("useDebounceEmailValidation ? ")
// State and setters for debounced value
const [valid, setValid] = useState(true);
const initRun = useRef(true);
console.log("init run = " , initRun.current)
useEffect(
() => {
console.log("useEffect?"); //---------------------------> this not happening on each render
//we don't want to do it on initial running
if(initRun.current){
initRun.current = false;
}
else{
// Update debounced value after delay
const handler = setTimeout(() => {
console.log("validating mail - " ,value);
setValid(validateEmail(value));
// setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
}
},
[value, delay] // Only re-call effect if value or delay changes
);
return valid;
}
the form component:
import React, {useLayoutEffect, useRef, useState} from 'react';
import Button from 'react-bootstrap/Button';
import {useDebounceEmailValidation} from "./utils-hooks";
import {Alert, Col, Form} from "react-bootstrap";
export function SubscribersForm() {
const [details, setDetails] = useState({
firstName: "",
lastName: "",
email: "",
tel: ""
});
const [popupMsg, setPopupMsg] = useState("default");
const [showMsg, setShowMsg] = useState(false);
const [isError, setIsError] = useState(false);
const emailChange = useRef(false);
const [validated, setValidated] = useState(false);
//For cases we need to access the email through local state and not by state
// (the value will persist between component re-rendering and the reference updating won't trigger a component re-rendering)
const emailRef = useRef("");
const validEmail = useDebounceEmailValidation(emailRef.current, 600);
// // general layout effect - will be happen on each component update - for DEBUG
// useLayoutEffect(() => {
// console.log("details = ", details);
// });
//happening after change in the popup message
useLayoutEffect(() => {
setTimeout(() => {
//resetting the msg
setPopupMsg("");
setShowMsg(
false);
}, 2000);
}, [popupMsg])
//happen after change in the details
useLayoutEffect(() => {
//handling email changing (validation)
if (emailChange.current) {
emailRef.current = details.email;
console.log("email.current = " , emailRef.current)
}
}, [details]);
const handleChange = (ev) => {
ev.persist();
if (ev.target.name === "email" ) {
emailChange.current = true;
} else {
emailChange.current = false;
}
setDetails(prevDetails => ({
...prevDetails,
[ev.target.name]: ev.target.value
}));
}
const onSubmit = (ev) => {
const form = ev.currentTarget;
//The default validation for the form
if (form.checkValidity() === false || !validEmail) {
ev.preventDefault();
ev.stopPropagation();
setValidated(true);
return;
}
ev.preventDefault();
alert("Those are the details - you can send it from here to the server !!! :) \n\n" +
"name = " + details.firstName + " " + details.lastName
+ "\nemail = " + details.email
+ "\nphone = " + details.tel);
//we set validation to false, because by default you don't want to show validation
setValidated(false);
setPopupMsg("Your details have been successfully saved");
setIsError(false);
setDetails({
firstName: "",
lastName: "",
email: "",
tel: ""
});
setShowMsg(true);
}
return (
<div className="subscribers-input">
<h3>Subscribers - Form</h3>
<Form className="needs-validation" noValidate validated={validated}
onSubmit={onSubmit}>{/*start of the form block */}
<Form.Row className="">{/*start of the form row of 12/12 columns*/}
<Col xs={12} className="">
<Form.Group controlId="firstName" className="">
<Form.Control
type="text"
placeholder="First name"
value={details.firstName}
onChange={handleChange}
name="firstName"
required
size="sm"
aria-label="first name"
/>
</Form.Group>
</Col>
</Form.Row>
<Form.Row className="">
<Col xs={12} className="">
<Form.Group controlId="lastName" className="">
<Form.Control
type="text"
placeholder="Last name"
value={details.lastName}
onChange={handleChange}
name="lastName"
required
size="sm"
aria-label="last name"
/>
</Form.Group>
</Col>
</Form.Row>
<Form.Row className="">
<Col xs={12} className="">
<Form.Group controlId="email" className="">
<Form.Control
type="email"
placeholder="Email"
value={details.email}
onChange={handleChange}
name="email"
required
size="sm"
aria-label="email"
isInvalid={!validEmail}
/>
<Form.Control.Feedback type="invalid">Email is Invalid</Form.Control.Feedback>
</Form.Group>
</Col>
</Form.Row>
<Form.Row className="">
<Col xs={12} className="">
<Form.Group controlId="tel" className="">
<Form.Control
type="tel"
placeholder="Phone"
value={details.tel}
onChange={handleChange}
name="tel"
required
size="sm"
aria-label="phone"
/>
</Form.Group>
</Col>
</Form.Row>
{showMsg &&
<Alert variant={isError ? 'danger' : 'success'}>{popupMsg}</Alert>
}
<Button type="submit" size="sm">Save</Button>
</Form>
</div>
)
}
See how the log not always happening.

there are two things to know: first, custom hooks only rerun when the component that we use our custom hook in (in our case, "SubscribersForm"), rerenders. second, the useEffect dependency array checks the equality of objects by references.
so to be sure that useEffect can intercept changes in its dependency array object we should pass new references for that object.
in the main component "SubscribersForm" you pass a reference to your "useDebounceEmailValidation" custom hook, so when you change the value of emailRef.current, no rerenders happen, and also the reference of the object is the same as before. you should use states in these cases

Related

React form not calling submit function upon submit button click

I have a straight forward react form below which called a handlesubmit function when the submit button is clicked. However, it seems that there are two issues. Firstly, the form doesn't validate inputs, and in fact accepts the empty inputs when submitted. Any help is appreciated please.
import Form from "react-bootstrap/Form"
import Button from "react-bootstrap/Button"
import Col from "react-bootstrap/Col"
import { useState } from "react"
const Register = () => {
const [validated, setValidated] = useState(false);
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const handleSubmit = (event) => {
const form = event.currentTarget;
console.log("in handlesubmit: " + form.checkValidity())
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
setValidated(true);
console.log("in handlesubmit: " + validated)
};
console.log('First Name: ' + firstName);
console.log('Last Name: ' + lastName);
return (
<div>
<h1>Registration page</h1>
<Form noValidated validated={validated} onSubmit={handleSubmit} style={{textAlign:'left'}}>
<Form.Row>
<Form.Group as={Col} controlId="formGridFirstName">
<Form.Label>First name</Form.Label>
<Form.Control type="text" placeholder="Enter first name" value={firstName} onChange={(e) => setFirstName(e.target.value)}/>
</Form.Group>
<Form.Group as={Col} controlId="formGridLastName">
<Form.Label>Last name</Form.Label>
<Form.Control type="text" placeholder="Enter last name" value={lastName} onChange={(e) => setLastName(e.target.value)}/>
</Form.Group>
</Form.Row>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
</div>
)
}
export default Register
I think you probably need to add an onclick to your button. Try onClick={()=>{}} and see if that's triggers your form onsubmit?

onChange event doesn't fire on first input

I'm new to React and I've got this registration (parent component) where it has an inventory (child component) and I'm changing the state of the inventory once the user inputs the quantity of the item that they hold so I can register them
But onChange seems to be delayed, I input 4 water bottles for example, and it console logs me the default value, only when I input the amount for another item like food it displays me the 4 water bottles and 0 food :(
This is what I'm working with...
Child component:
import React from "react";
import { Col, Row, FormGroup, Label, Input } from "reactstrap";
import waterIcon from "./../assets/water.png";
import soupIcon from "./../assets/food.png";
import medsIcon from "./../assets/aid.png";
import weaponIcon from "./../assets/gun.png";
function Inventory({ onSubmitInventory, currentInventory }) {
const onChangeWater = (e) => {
const newInventory = { ...currentInventory, water: e.target.value };
onSubmitInventory(newInventory);
};
const onChangeSoup = (e) => {
const newInventory = { ...currentInventory, soup: e.target.value };
onSubmitInventory(newInventory);
};
const onChangeMeds = (e) => {
const newInventory = { ...currentInventory, meds: e.target.value };
onSubmitInventory(newInventory);
};
const onChangeWeapon = (e) => {
const newInventory = { ...currentInventory, weapon: e.target.value };
onSubmitInventory(newInventory);
};
return (
<FormGroup>
<Row className="justify-content-center text-center border-top pt-3">
<Col xs="3">
<img src={waterIcon} alt="Water" />
<Label for="inventoryWater">Fiji Water:</Label>
<Input
type="number"
name="inventoryWater"
id="inventoryWater"
placeholder="Enter amount..."
onChange={onChangeWater}
/>
</Col>
<Col xs="3">
<img src={soupIcon} alt="Soup" />
<Label for="inventorySoup">Campbell Soup:</Label>
<Input
type="number"
name="inventorySoup"
id="inventorySoup"
placeholder="Enter amount..."
onChange={onChangeSoup}
/>
</Col>
<Col xs="3">
<img src={medsIcon} alt="Aid" />
<Label for="inventoryMeds">First Aid Pouch:</Label>
<Input
type="number"
name="inventoryMeds"
id="inventoryMeds"
placeholder="Enter amount..."
onChange={onChangeMeds}
/>
</Col>
<Col xs="3">
<img
className="d-block"
style={{ margin: "0 auto" }}
src={weaponIcon}
alt="Gun"
/>
<Label for="inventoryWeapon">AK47:</Label>
<Input
type="number"
name="inventoryWeapon"
id="inventoryWeapon"
placeholder="Enter amount..."
onChange={onChangeWeapon}
/>
</Col>
</Row>
</FormGroup>
);
}
export default Inventory;
Parent component:
import React, { useState } from "react";
import { Col, Row, Button, Form, Label, Input } from "reactstrap";
import { useForm } from "react-hook-form";
import Inventory from "./Inventory";
import MapContainer from "./MapContainer";
function Register() {
const [lonlat, setLonlat] = useState("");
const onMarkerChange = (lonlat) => {
setLonlat(lonlat);
};
const [inventory, setInventory] = useState({
water: 0,
soup: 0,
meds: 0,
weapon: 0,
});
const onSubmitInventory = (newInventory) => {
setInventory(newInventory);
console.log(inventory);
};
const { register, handleSubmit, errors } = useForm();
const onSubmit = (data) => {
console.log(inventory);
const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: data.personName }),
};
console.log(data);
};
return (
<Form
id="registerForm"
name="registerForm"
onSubmit={handleSubmit(onSubmit)}
>
<h4>New Person</h4>
<Row className="border-top pt-4">
<Col xs="8">
<Label for="personName">Your name:</Label>
<Input
className="gray-input"
type="text"
name="name"
id="personName"
placeholder="Enter your name here..."
innerRef={register}
/>
</Col>
<Col xs="2" className="text-center">
<Label for="personAge">Age:</Label>
<Input
className="gray-input"
type="number"
name="age"
id="personAge"
placeholder="Enter age..."
innerRef={register}
/>
</Col>
<Col xs="2" className="text-center">
<Label for="personGender">Gender:</Label>
<Input
type="select"
name="gender"
id="personGender"
innerRef={register}
>
<option defaultValue disabled>
-
</option>
<option>F</option>
<option>M</option>
</Input>
</Col>
</Row>
<Row>
<Col xs="12">
<Input
hidden
id="personLatLon"
name="personLatLon"
type="text"
defaultValue={lonlat}
innerRef={register}
/>
<MapContainer onMarkerChange={onMarkerChange} />
</Col>
</Row>
<h4>Inventory</h4>
<Inventory
onSubmitInventory={onSubmitInventory}
currentInventory={inventory}
/>
<Button
outline
color="secondary"
className="mt-2"
type="submit"
form="registerForm"
>
Submit
</Button>
</Form>
);
}
export default Register;
EDIT: Updated code with tips from answers, still facing the same problem :(
The only problem I see with your code is that you are trying to pass a second argument to setInventory. I don't believe this works with hooks like it did with class components. When I attempt to type that in typescript it throws an instant error.
Just pass the actual data you are trying to send and then call onSubmitInventory(inventory) for example:
const onChangeWeapon = (e) => {
const newInventory = {...inventory, weapon: e.target.value};
setInventory(newInventory);
onSubmitInventory(newInventory);
};
If you have to wait for the next render to call onSubmitInventory, it should be a useEffect in the parent. My next question would be why the parent needs the state and the child also has it as a state? Possibly it should be lifted up. But I'm not sure of that without seeing the parent.
Edit: Is your problem just with the console.log? If you run
const [state, setState] = useState(true);
// ...
setState(false);
console.log(state); // outputs true.
The value of state does not change until the next render. Calling setState will cause a new render, but until that happens, the old value is there. Add a console.log of state in the parent just in the body like here
const [inventory, setInventory] = useState({
water: 0,
soup: 0,
meds: 0,
weapon: 0,
});
console.log('I just rendered, inventory: ', inventory);
You'll see it updates!
The fact that the value of state does not change can bite you in this case:
const [state, setState] = useState(true); // closes over this state!
// ...
setState(!state); // closes over the state value above
setState(!state); // closes over the same state value above
// state will be false on next render
So I use this form if there is any possibility I call setState twice before a render:
const [state, setState] = useState(true);
// ...
setState(state => !state);
setState(state => !state);
// state will be true on next render
Inside your parent component's onSubmitInventory you have named the argument inventory but that already exists in the parent's scope. I suggest renaming that newInventory to be clear about which you are referencing.
Also, it seems like you are keeping track of inventory in both the parent and child's state. Keep it in the parent and pass it down to the child as a prop/props.

behavior not expected but working, I need some explanation please

I am on the CRUD step with hooks and everything is working but I don't understand why the Axios.post doesn't need .then in this case.
If I send only customer instead customer[0] nothing happens, then the .then(response => console.log(response)) returns nothing. I guess that the customer[0] has already the right format: [{}].
import React, { useState, useEffect } from 'react';
import Axios from 'axios';
import { Form, Container, Col, Row, Button } from 'react-bootstrap';
// import data
import fields from './customerFields'; // <= array of object
function AddCustomers() {
const [customer, setCustomer] = useState([{}]);
const [inputValue, setInputValue] = useState('');
useEffect(() => {
setCustomer([inputValue]);
}, [inputValue]);
const handleSubmit = (event) => {
event.preventDefault();
const newCustomer = [...customer, inputValue];
setCustomer(newCustomer);
const url = 'http://localhost:5000/api/clients';
Axios.post(url, customer[0])
};
const handleChange = (event) => {
event.preventDefault();
const { value } = event.target;
const { name } = event.target;
const newValue = { ...inputValue, [name]: value };
setInputValue(newValue);
};
// return
return (
<Container>
<Row>
<Col className="col-form-label-sm">
<h3 id="">Identité du client</h3>
<Form
action=""
className="form-group"
onSubmit={(event) => handleSubmit(event)}
>
<Form.Group>
<Form.Label>{fields[0].label}</Form.Label>
<Form.Control
name={fields[0].name}
type="text"
value={inputValue.name}
onChange={(event) => handleChange(event)}
/>
</Form.Group>
<Form.Group>
<Form.Label>{fields[1].label}</Form.Label>
<Form.Control
name={fields[1].name}
type="text"
value={inputValue.name}
onChange={(event) => handleChange(event)}
/>
</Form.Group>
<Form.Group>
<Form.Label>{fields[2].label}</Form.Label>
<Form.Control
name={fields[2].name}
type="text"
value={inputValue.name}
onChange={(event) => handleChange(event)}
/>
</Form.Group>
<Button type="submit" variant="warning">
Ajouter un client
</Button>
</Form>
</Col>
</Row>
</Container>
);
}
export default AddCustomers;
You're making an asynchronous operation (Axios.post(url, customer[0])) and not waiting for it to resolve, so you're just leaving the promise floating there. If you don't care if the operation succeed or failed that is fine, but in most cases you want to either handle the error received or do something with the result.
About why Axios.post is accepting customer[0] that's because it accepts anything in the second parameter it can send in a POST request. You're always setting your customer as an array with the inputValue variable inside (setCustomer([inputValue]);) so inputValue is what you send as customer[0].
ohhh! I got it, I think this version is better :
const handleSubmit = (event) => {
event.preventDefault();
const url = 'http://localhost:5000/api/clients';
Axios.post(url, customer);
};
thank you #jonrsharpe

useState updated state not available in the same handler even with timeout [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

How select checkbox in Reactjs?

I am trying when I click on a checkbox it should get selected and save its value true in localstorage so that if the page is refreshed it should get value from the localstorage, similarly for second checkbox if it is selected too then also save its value true in localstorage.
In simple way if I select a both the checkboxes it should retain even after page refresh this is what I am trying for
Here is my code is what I have tried
Link - https://codesandbox.io/s/musing-architecture-p2nrg?file=/src/App.js:0-1760
import React from "react";
import "./styles.css";
import { Form } from "react-bootstrap";
import "bootstrap/dist/css/bootstrap.min.css";
export default function App() {
const data = {
naming: localStorage.getItem("naming") || false,
fullname: localStorage.getItem("fullname") || false
};
const [currentCheckboxId, setCheckboxId] = React.useState(data);
const setCheckbox = event => {
const naming = event.target.checked;
console.log(naming);
localStorage.setItem("naming", naming);
setCheckboxId({
...data,
naming: event.target.checked
});
};
const setCheckbox2 = event => {
const fullname = event.target.checked;
console.log(fullname);
localStorage.setItem("fullname", fullname);
setCheckboxId({
...data,
fullname: event.target.checked
});
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Form>
<>
<Form.Check
onChange={setCheckbox}
type="checkbox"
label="Check me out"
id="first"
checked={currentCheckboxId.naming}
/>
<Form.Group controlId="email">
<Form.Label>Email Address</Form.Label>
<Form.Control type="text" placeholder="Enter email" />
</Form.Group>
</>
<>
<Form.Check
onChange={setCheckbox2}
type="checkbox"
label="Check me out"
id="second"
checked={currentCheckboxId.fullname}
/>
<Form.Group controlId="fullname">
<Form.Label>Name</Form.Label>
<Form.Control type="text" placeholder="Enter name" />
</Form.Group>
</>
</Form>
</div>
);
}
Here is what you need to do:
Initialize the state with false
Use useEffect to run at mounted and retrieve checkbox values from LocalStorage and setState accordingly
Use setState with updater function to set new state which depends on current state
export default function App() {
// 1. Initially "false"
const [currentCheckboxId, setCheckboxId] = React.useState({
naming: false,
fullname: false
});
// 2. useEffect to run # mounted:
// get from LS and update the state
React.useEffect(() => {
const data = {
naming: localStorage.getItem('naming') === 'true' ? true : false,
fullname: localStorage.getItem('fullname') === 'true' ? true : false
};
setCheckboxId(data);
}, []);
const setCheckbox = event => {
const naming = event.target.checked;
console.log('naming', naming);
localStorage.setItem('naming', naming);
// 3. use "function" with prevData as first argument to setState
setCheckboxId(prevData => ({
...prevData,
naming: naming
}));
};
const setCheckbox2 = event => {
const fullname = event.target.checked;
console.log('fullname', fullname);
localStorage.setItem('fullname', fullname);
// 3. same as above
setCheckboxId(prevData => ({
...prevData,
fullname: fullname
}));
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Form>
<>
<Form.Check
onChange={setCheckbox}
type="checkbox"
label="Check me out"
id="first"
checked={currentCheckboxId.naming}
/>
{/* Rest of your code */}
}
Here is a playground.

Resources