React Context Updates Not Firing During OnSubmit Functions? - javascript

I am using the Context API to add user details from a form to a global state. When I submit the form, the state is always "one step behind" - essentially, a double click is required to get the desired result.
Basic recreation of the code (have removed irrelevant bits and some imports):
import { UserProvider, UserContext } from "../../StateContext"
export default function SignUp() {
const user = useContext(UserContext)
const history = useHistory()
const handleSubmit = (e) => {
e.preventDefault()
user.setName(userDetails.name)
//logging out the user object here will result in the previous values shown
}
const [userDetails, setUserDetails] = useState({
name: null,
age: null,
})
return (
<>
<form onSubmit={handleSubmit}>
<div className="form-vertical-batch">
<FormControl>
<Input
type="text"
placeholder="Your Name"
required={true}
onChange={(e) =>
setUserDetails({ ...userDetails, name: e.target.value })
}
></Input>
<FormHelperText>
Put the name you're best known by online - either a nickname,
brand name, or your real name.
</FormHelperText>
</FormControl>
<FormControl>
<TextField
type="number"
inputProps={{ min: 18, max: 99 }}
onChange={(e) =>
setUserDetails({ ...userDetails, age: e.target.value })
}
/>
<FormHelperText>
You currently must be at least 18 years old to use the platform.
</FormHelperText>
</FormControl>
</div>
</div>
<input
id="formButton"
className="btn sign-up-button"
type="submit"
placeholder="Send message"
/>
</form>
</>
)
}
To clarify the issue here - if I submit with a name as "Reikon" and log our the user object, the first time it will return as null, and then the second time it will return "Reikon" as expected.

Related

random number not changing on submit in React form

I have built a travel journal however I ran into two big bugs, one of them where the id which is set to Math.random()*10000 and is expected to change on submit however it does not, another issue I have is where once I remove one journal entry, I am not able to add any more entries via submit.
I have tried adding the math.random in different places however it doesn't change, I have run out of ideas on how to tackle this issue, if you have any suggestions ,any help is appreciated.
import React, { useState } from "react";
import Card from "./Card";
import data from "./data";
function Entry(props) {
const [entry, setEntry] = useState([
{
title: "",
location: "",
googleMapsUrl: "",
startDate: "",
endDate: "",
description: "",
imageUrl: "",
id: Math.random() * 100000000,
},
]);
function handleChange(e) {
setEntry((prevState) => {
return {
...prevState,
[e.target.name]: e.target.value,
};
});
}
// const newData = [...data];
function handleSubmit(e) {
e.preventDefault();
setEntry((prevState) => {
return {
...prevState,
};
});
data.unshift(entry);
}
return (
<div>
<form className="entry-form">
<h1 className="entry-title">Add another Travel Memory</h1>
<div className="journal-entry">
<input
className="entry-input"
type="text"
value={entry.location}
name="location"
placeholder="LOCATION"
onChange={handleChange}
required
/>
<input
className="entry-input"
type="text"
name="title"
value={entry.title}
placeholder="LANDMARK"
onChange={handleChange}
required
/>
<input
className="entry-input"
type="text"
name="googleMapsUrl"
value={entry.googleMapsUrl}
placeholder="GOOGLE MAPS LINK"
onChange={handleChange}
required
/>
<input
className="entry-input"
type="date"
value={entry.startDate}
name="startDate"
onChange={handleChange}
required
/>
<input
className="entry-input"
type="date"
value={entry.endDate}
name="endDate"
onChange={handleChange}
required
/>
<textarea
className="entry-input"
placeholder="ADD YOUR STORY OR A FUN FACT FROM YOUR JOURNEY"
name="description"
value={entry.description}
onChange={handleChange}
required
/>
<input
className="entry-input"
type="text"
name="imageUrl"
value={entry.imageUrl}
placeholder="ADD A IMAGE LINK TO REMIND YOU OF YOUR TRAVEL"
onChange={handleChange}
/>
<button type="submit" onClick={handleSubmit} className="entry-btn">
add your travel memory
</button>
</div>
</form>
<Card data={data} />
</div>
);
}
export default Entry;
Math.random()*10000 and is expected to change on submit however it does not
Because no code was written to change it. Take a look at the state update in the submit handler:
setEntry((prevState) => {
return {
...prevState,
};
});
No values are changed. The new state is an exact copy of the previous state. Contrast this with the state update in the change handler for the input fields:
setEntry((prevState) => {
return {
...prevState,
[e.target.name]: e.target.value,
};
});
Notice how the new state is constructed from the previous state, and a given field is updated.
If you want to update the id field in the submit handler, update the id field in the submit handler:
setEntry((prevState) => {
return {
...prevState,
id: Math.random() * 100000000
};
});

React dropdown with options from Firebase realtime db (Formik + materialUI)

I'm having trouble with the dropdown that maps through the data I fetch from the Firebase realtime db and assigns selected value to the Formik form in order to save it to another object in the database.
I have restaurants object with pushId key containing name, city, and id (pushId key), and I have dishes with pushId key containing name, price, id (pushId key), restaurantId (id of the restaurant).
*Here I need another key:value pair that will be for example restaurantId: "-MCvTh91-qHLcy09ZpYm"
I did a fetch of restaurants using useEffect hook:
const [restaurants, setRestaurants] = useState([])
useEffect(() => {
let restQuery = Firebase.database().ref('restaurants').orderByKey()
restQuery.once('value').then(function(snapshot) {
snapshot.forEach(function(childSnapshot) {
setRestaurants(prevState => [...prevState, childSnapshot.val()])
})
})
},[])
But I simply can't map the restaurants array to the dropdown form options and if I select a restaurant save its id to the Formik value
<Formik
initialValues={{
name: '',
price: '',
restaurantId: '',
}}
onSubmit = {(data, {resetForm}) => {
console.log('submit: ', data)
resetForm()
}}>
{({values, handleChange, handleSubmit}) => (
<Form onSubmit={handleSubmit}>
<Field name="name" type="input" as={TextField} /><br/>
<Field name="price" type="input" as={TextField} /><br/>
<div>Restaurants:</div>
<Field type="select" as={Select}>
{restaurants.map((rest) =>
<MenuItem name="restaurantId" value={rest.id}>
{rest.name}
</MenuItem>
)}
</Field>
<div>
<Button type="submit">Submit</Button>
</div>
</Form>
)}
</Formik>
This gives mi selection of the restaurants in the dropdown like:
However, when I click on the option the error console throws these warnings and nothing happens
Warning: Formik called `handleChange`, but you forgot to pass an `id` or `name` attribute to your input: undefined
Formik cannot determine which value to update. For more info see https://github.com/jaredpalmer/formik#handlechange-e-reactchangeeventany--void
Material-UI: You have provided an out-of-range value `[object Object]` for the select component.
Consider providing a value that matches one of the available options or ''.
The available values are `-MCvTh91-qHLcyO9ZpYm`, `-MCxfsFz6pSaokkbBwpA`, `-MCxg6CB_U4HaCE659Tw`.
I'm stuck on these and anything I try doesn't work.
As answered by #Chris G in the comments the issue was in placement of the name attribute, it should be in the select element not the option element:
{({values, handleChange, handleSubmit}) => (
<Form onSubmit={handleSubmit}>
<Field name="name" type="input" as={TextField} /><br/>
<Field name="price" type="input" as={TextField} /><br/>
<div>Restaurants:</div>
<Field type="select" name="restaurantId" as={Select}>
{restaurants.map((rest) =>
<MenuItem key={rest.id} value={rest.id}>
{rest.name}
</MenuItem>
)}
</Field>
<div>
<Button type="submit">Submit</Button>
</div>
</Form>
)}

How to give validation in multi step form using react

I am working on a scenario where I have to do a multi-step form which I have already done, as well as the validation part. I am using react-hook-form for validation.
I have multi-step form:
in the first form I have several fields and one radio button
by default radio button is ticked on for auto generated pass so in this case I have nothing to do
the second one is let me create a password so in this case one input field will be show and the user will create the password
Issue
In my final form I am doing the validation like below:
{
fields: ["uname", "email", "password"], //to support multiple fields form
component: (register, errors, defaultValues) => (
<Form1
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
},
So to validate uname, email and password I am passing the values like above.
But when the radio button is ticked for auto generated password it is still handling the validation, I click on next and it is not going to next for because of password field.
And if I check the radio button as let me create the password it goes to next form and when I came back by clicking back it is going to auto generated password again and it is not holding the previous state. For other input fields it is handling the previous values but not in case of radio button scenario.
My full working code sandbox
Answer 1 The reason is you fields: ["uname", "email", "password"] is fixed, password is always to be taken validation.
Solution Need to store state of Form1 in App so you can check if the state of auto generated password is on remove password from the list
App.js
... other code
// need to move state and function form Form to app
const [show_input, setshow_input] = useState(false);
const createInput = () => {
setshow_input(true);
};
const auto_text = () => {
setshow_input(false);
};
const forms = [
{
// validate based on show_input state
fields: show_input ? ["uname", "email", "password"] : ["uname", "email"], //to support multiple fields form
component: (register, errors, defaultValues) => (
<Form1
register={register}
errors={errors}
defaultValues={defaultValues}
auto_text={auto_text}
createInput={createInput}
show_input={show_input}
/>
)
},
{
fields: ["lname"],
component: (register, errors, defaultValues) => (
<Form2
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
},
{
component: (register, errors, defaultValues) => (
<Form3
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
}
];
... other code
Answer 2 When you go next the Form1 is unmounted so its state is destroyed. When you store Form1's state in App.js you will fix this issue too
Bonus: It's prefered to use camalCase (E.g: showInput) rather than underscore (show_input)
The main problem is that you render the forms conditionally so all the previous form values are removed. The solution for this is to keep all forms mounted and just use display: none or display: block depending on which form is selected. This way all values will be persisted whenever you go to next or prev form or submit the form.
The second problem that you didn't remove the password field when it's unmounted so when moveToNext is called the valid argument in triggerValidation callback is always false. I fixed that by setting the fields for Form1 conditionally depending on if the password input is visible or not.
The third problem you are using defaultValues for the wrong purpose. You can get the current form values using getValues() which will return all the current values of the form.
I set the default value for uname field just as an example to show you how defaultValues should be used.
you can check the full solution here: https://codesandbox.io/s/fragrant-forest-75pzs?file=/src/App.js
here are all the changed files:
App.js
import React, { useState } from "react";
import Form1 from "./components/Form1";
import Form2 from "./components/Form2";
import Form3 from "./components/Form3";
import { useForm } from "react-hook-form";
function MainComponent() {
const {
register,
triggerValidation,
defaultValues,
errors,
getValues
} = useForm({
// You can set default values here
defaultValues: {
uname: "Lol"
}
});
console.log("Errors: ", errors);
const [currentForm, setCurrentForm] = useState(0);
// control password input visibility and Form1 fields
const [passwordVisible, setPasswordVisible] = useState(false);
const showPassword = () => {
setPasswordVisible(true);
};
const hidePassword = () => {
setPasswordVisible(false);
};
const forms = [
{
fields: passwordVisible
? ["uname", "email", "password"]
: ["uname", "email"],
component: (register, errors) => (
<Form1
// a key is needed to render a list
key={0}
// this will be used to set the css display property to block or none on each form
shouldDisplay={currentForm === 0}
register={register}
errors={errors}
showPassword={showPassword}
hidePassword={hidePassword}
passwordVisible={passwordVisible}
/>
)
},
{
fields: ["lname"],
component: (register, errors) => (
<Form2
key={1}
shouldDisplay={currentForm === 1}
register={register}
errors={errors}
/>
)
},
{
component: (register, errors) => (
<Form3
key={2}
shouldDisplay={currentForm === 2}
register={register}
errors={errors}
values={getValues()}
/>
)
}
];
const moveToPrevious = () => {
triggerValidation(forms[currentForm].fields).then(valid => {
if (valid) setCurrentForm(currentForm - 1);
});
};
const moveToNext = () => {
triggerValidation(forms[currentForm].fields).then(valid => {
if (valid) setCurrentForm(currentForm + 1);
});
};
const prevButton = currentForm !== 0;
const nextButton = currentForm !== forms.length - 1;
const handleSubmit = e => {
console.log("whole form data - ", getValues());
};
return (
<div>
<div className="progress">
<div>{currentForm}</div>
</div>
{forms.map(form => form.component(register, errors))}
{prevButton && (
<button
className="btn btn-primary"
type="button"
onClick={moveToPrevious}
>
back
</button>
)}
{nextButton && (
<button className="btn btn-primary" type="button" onClick={moveToNext}>
next
</button>
)}
{currentForm === 2 && (
<button
onClick={handleSubmit}
className="btn btn-primary"
type="submit"
>
Submit
</button>
)}
</div>
);
}
export default MainComponent;
Form1
import React from "react";
function Form1({
register,
errors,
shouldDisplay,
passwordVisible,
showPassword,
hidePassword
}) {
return (
<div style={{ display: shouldDisplay ? "block" : "none" }}>
<form autoComplete="on">
<br />
<div className="form-group">
<label>User name</label>
<input type="text" name="uname" ref={register({ required: true })} />
{errors.uname && <span>required</span>}
<label>Email</label>
<input type="email" name="email" ref={register({ required: true })} />
{errors.email && <span>required</span>}
</div>
<div>
<div className="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
<label className="form_label">Password</label>
<div className="form-check">
<label>
<input
type="radio"
name="auto_pass"
id="Radios1"
value="auto_pass"
className="form-check-input"
defaultChecked={true}
onChange={hidePassword}
/>
Auto generated password
</label>
</div>
<div className="form-check">
<label>
<input
type="radio"
name="auto_pass"
id="Radios2"
value="let_me"
className="form-check-input"
onChange={showPassword}
/>
Let me create the password
</label>
</div>
</div>
{passwordVisible && (
<div className="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12 mb-3">
<label className="form_label">Password</label>
<input
type="password"
name="password"
className="form-control"
ref={register({ required: true })}
/>
{errors.password && (
<span className="text-danger">Password is reguired</span>
)}
</div>
)}
</div>
</form>
</div>
);
}
export default Form1;
Form2
import React from "react";
function Form2({ register, errors, shouldDisplay }) {
return (
<div style={{ display: shouldDisplay ? "block" : "none" }}>
<form autoComplete="on">
<br />
<div className="form-group">
<label>User last name</label>
<input type="text" name="lname" ref={register({ required: true })} />
{errors.lname && <span>required</span>}
</div>
</form>
</div>
);
}
export default Form2;
Form3
import React from "react";
function Form3({ values, shouldDisplay }) {
return (
<div style={{ display: shouldDisplay ? "block" : "none" }}>
<h3>Want to display all values here like below</h3>
{Object.entries(values).map(([key, value]) => (
<p key={key}>
{key}: {value}
</p>
))}
<br />
<p>So that use can check for any Wrong info</p>
</div>
);
}
export default Form3;

React Form: How to add error message that disappear if the input was typed in

I already built the form in React and it shows the input fields in red borders that'll change to regular borders once someone types it in. I used this example from this React form article link So everything is working except I wanted to add the error message under the input field that displays "Please fill in the blank field" that will disappear once someone starts typing in the field. How do I do this?
Here's my code in Form.js:
import React, { Component } from 'react';
import FormField from './FormFieldBox';
function validate(name, isin) {
// true means invalid, so our conditions got reversed
return {
name: name.length === 0,
isin: isin.length === 0
};
}
export default class PopupForm extends Component {
constructor(props) {
super(props)
this.state = {
name: '',
isin: '',
country: '',
errormessage: ''
}
}
updateInput = (e) =>{
this.setState({[e.target.name]: e.target.value})
}
closePopupSubmit = (e) => {
if (!this.canBeSubmitted()) {
e.preventDefault();
}
let security = { //1.gather security data from form submit
name: this.state.name,
isin: this.state.isin,
country: this.state.country
}
this.props.submitPopup(security); //2.closePopup function, add security data
}
canBeSubmitted() {
const errors = validate(this.state.name, this.state.isin);
const isDisabled = Object.keys(errors).some(x => errors[x]);
return !isDisabled;
}
cancelPopupSubmit = (e) => {
e.preventDefault()
this.props.cancelPopup();
}
render() {
const errors = validate(this.state.name, this.state.isin);
const isDisabled = Object.keys(errors).some(x => errors[x]);
return (
<div className='popup'>
<div className='popup-inner'>
<form onSubmit={this.closePopupSubmit}>
<FormField onChange={this.updateInput} className={errors.name ? "input error" : "input"} label="Name" type="text" name="name" value={this.state.name} />
<FormField onChange={this.updateInput} className={errors.isin ? "input error" : "input"} label="ISIN" type="text" name="isin" value={this.state.isin} />
<FormField onChange={this.updateInput} label="Country" type="text" name="country" value={this.state.country} />
<button type="button" onClick={this.cancelPopupSubmit} className="button">Cancel</button>
<button type="submit" className="button" disabled={isDisabled}>Submit</button>
</form>
</div>
</div>
)
}
}
And my component FormField.js
import React from "react";
const FormBox = props => {
return (
<div className="field">
<label className="label">{props.label}</label>
<div className="control">
<input onChange={props.onChange}
className={props.className}
type={props.type}
name={props.name}
value={props.value}
placeholder={props.placeholder} />
{/* {props.errormessage} */}
</div>
</div>
)
}
export default FormBox;
const FormBox = props => {
return (
<div className="field">
<label className="label">{props.label}</label>
<div className="control">
<input onChange={props.onChange}
className={props.className}
type={props.type}
name={props.name}
value={props.value}
placeholder={props.placeholder} />
</div>
{Boolean(props.value.length) || (
<div className="err-msg">
Please fill in the blank field
</div>
)}
</div>
)
}
There are two ways you can achieve this
First : oninvalid attribute in HTML5 and calling a custom function on that.
Second : along with each element name object in state have a length attribute. In validation function you can check for the length and throw a custom error that you want to display.

Not able to receive data in the handlechange in React JS

I have a form in Reactjs where I am expected to collect the information and sent to my API. This information include, Phonenumber, gender, date of birth and country of birth.
I have written my react handleChange code as follows:
handleChange (event) {
const { name, value } = event.target;
const { info } = this.state;
this.setState({
info: {
...info,
[name]: value
}
});
}
Then I have written my return function as follows
return (
<CardBody>
<form name="form" onSubmit={this.handleSubmit}>
<div className="grey-text">
<PhoneInput
placeholder="Enter phone number"
value={ info.phone_number }
onChange={this.handleChange}
error={ info.phone_number ? (isValidPhoneNumber(info.phone_number) ? undefined : 'Invalid phone number') : 'Phone number required' }/>
<Select
value={info.gender}
onChange={this.handleChange}
options={sex}
/>
<DatePicker
selected={info.dbirth}
onChange={this.handleChange}
peekNextMonth
showMonthDropdown
showYearDropdown
dropdownMode="select"
/>
<CountryDropdown
value={info.nationality}
onChange={this.onChangeh} />
</div>
<div style={{backgroundColor:"#006400"}}>
<Button type="submit" onClick={this.handleSubmit} className=" btn-success btn-block" >Validate Account</Button>
</div>
</form>
</CardBody>
);
}
};
When I try compile my project it compiles well but when I try to select any of the dropdowns such as phone number, gender I get the error
TypeError: event is undefined or TypeError: _event$target is undefined
I am quite new to reactjs. I have tried looking for answer to no avail . Any help on this will be appreciated
you're getting the error because the <Select /> will pass the value to the onChange unlike the <Input /> which passes an event , one way to get around this it to pass a similar object :
<Select
value={info.gender}
onChange={value => this.handleChange({target : {name : 'mySelect', value }}) }
options={sex}
/>
( probably the same for the datePicker )
You can simply define any variable in state. I defined selectedOption and used this code. This should help
handleSelect = (selectedOption) => {
this.setState({
selectedOption
})
}
and call in onChange={this.handleSelect}

Categories

Resources