I have a problem with a React app. I have a form with two inputs, and when I submit the form with empty inputs, it should render an error message in each of them. The problem is that it doesn't show for the first input. How can I fix it to display an error in each of those? The implementation is in useForm.js.
FormUI image:
My code:
Form.js
const Form = () => {
const formLogin = () => {
console.log("Callback function when form is submitted!");
console.log("Form Values ", values);
}
const {handleChange, values, errors, handleSubmit} = useForm(formLogin);
return (
<Wrapper>
<form onSubmit={handleSubmit}>
<div className="govgr-form-group gap-bottom">
<label className="govgr-label govgr-!-font-weight-bold" htmlFor="code">Code*</label>
{errors.code && <p className="govgr-error-message"><span className="govgr-visually-hidden">Λάθος:</span>{errors.code}</p>}
<input className={`govgr-input govgr-!-width-three-quarter ${errors.code ? 'govgr-error-input' : ''}`} id="code" name="code" type="text" onChange={handleChange} />
</div>
<fieldset>
<div className="govgr-form-group">
<label className="govgr-label govgr-!-font-weight-bold" htmlFor="first">Name*</label>
{errors.first && <p className="govgr-error-message"><span className="govgr-visually-hidden">Λάθος:</span>{errors.first}</p>}
<input className={`govgr-input govgr-!-width-three-quarter ${errors.first ? 'govgr-error-input' : ''}`} id="first" name="first" type="text" onChange={handleChange} />
</div>
</fieldset>
<button type="submit" className="govgr-btn govgr-btn-primary btn-center">Save</button>
</form>
</Wrapper>
);
};
export default Form;
useForm.js:
const useForm = (callback) => {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const validate = (event, name, value) => {
event.persist();
switch (name) {
case "code":
if (value.trim() === "" || value.trim() === null) {
setErrors({
...errors,
code: "Code is required",
});
} else {
let newObj = omit(errors, "code");
setErrors(newObj);
}
break;
case "first":
if (value.trim() === "" || value.trim() === null) {
setErrors({
...errors,
first: "Name is required",
});
} else {
let newObj = omit(errors, "first");
setErrors(newObj);
}
break;
default:
break;
}
};
const handleChange = (event) => {
event.persist();
let name = event.target.name;
let val = event.target.value;
validate(event, name, val);
setValues({
...values,
[name]: val,
});
};
const handleSubmit = (event) => {
if (event) event.preventDefault();
if (
Object.keys(errors).length === 0 &&
Object.keys(values).length !== 0 &&
values.code &&
values.first
) {
callback();
} else {
if (!values.code) {
setErrors({
...errors,
code: "Code is required.",
});
}
if (!values.first) {
setErrors({
...errors,
first: "Name is required.",
});
}
}
};
return {
values,
errors,
handleChange,
handleSubmit
};
};
export default useForm;
You can simplify your code by having only a single validation routine. You can fix the error you mention, having only a single error at a time, by using the current state as passed into setState by the framework. The construct for this is
setState(e => /* whatever you want to return relative to e */);
Ultimately, the error you're seeing is because the component, and its code, is rerendered after every change of state (but they are batched for performance reasons), so your code is only seeing an older version of the state when you access it directly. If you want the actual current state when changing it, it's always best to have the framework pass it directly to the change-state function. If you always follow this pattern, you will rarely have any problems with the react state model.
The hard problem however isn't any of this. The hard problem is the way you're removing errors. First, from a UI perspective, it's a little inconsistent to only show a field as an error when it is edited, because the form is erroneous when it's first shown. But, ignoring that, the underlying technical problem is removing a property from an object. Normally we would use arrays rather than objects when we need to do this, or we would use underscore's omit function. I see in your code you call a function called omit, but you don't have an explanation as to what that is. In your case though, this can be easily solved by never removing the property. Instead, have each as an object with a valid flag. This would allow you to remove the switch statement completely.
You could go further and roll the value in there too, making a complete single state for each field, but I won't demonstrate that here.
I haven't tested this, so there may be a typo or two lurking in here.
const useForm = (callback) => {
const [values, setValues] = useState({code: '', first: ''});
const [errors, setErrors] = useState({first: {message: "Name is required", valid: true}, code: {message: "Code is required", valid: true}});
const isEmpty = val => val.trim() === "" || val.trim() === null);
const validate = (name, val) => {
setErrors(e => ({...e, [name]: {...e[name], valid: isEmpty(val)}}))
return errors[name].valid;
};
const handleChange = (event) => {
let name = event.target.name;
let val = event.target.value;
setValues({...values, [name]: val});
validate(name, val);
};
const handleSubmit = event => {
if (event) event.preventDefault();
const valid = validate('code', values.code) && validate('first', values.first);
if ( valid ) {
callback();
}
};
return {
values,
errors,
handleChange,
handleSubmit
};
};
export default useForm;
And, you'd have to change your HTML template slightly:
{!errors.first.valid && <p className="govgr-error-message"><span className="govgr-visually-hidden">Λάθος:</span>{errors.first.message}</p>}
I realise that this new validate function is a huge departure from what you had, but based on your own code I think you can handle it. Basically, it's just using destructuring and variable property accessors. This looks like its well within your capabilities to understand, but feel free to ask more questions if any of it is confusing or doesn't actually work :)
Related
const [name, setName] = useState("");
const [age, setAge] = useState("");
const initialValues = {
name: "",
age: "",
};
const [formValues, setFormValues] = useState(initialValues);
const [formErrors, setFormErrors] = useState({});
const [isSubmit, setIsSubmit] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({ ...formValues, [name]: value });
};
const handleSubmit = (e) => {
setFormErrors(validate(formValues));
setIsSubmit(true);
};
const validate = (values) => {
const errors = {};
if (!values.name) {
errors.name = "Name is required";
}
if (!values.age) {
errors.age= "Age is required";
}
return errors;
};
const userCreate = async () => {
await api.post("/createuser", {
name,
age,
});
};
return (
<div class="container">
<Form
onSubmit={
Object.keys(formErrors).length === 0 && isSubmit
? userCreate
: handleSubmit
}
>
<Form.Field>
<label>Name</label>
<input
name="name"
onChange={(e) => {
setName(e.target.value);
handleChange(e);
}}
values={formValues.name}
/>
<span className="error-message">{formErrors.name}</span>
</Form.Field>
<Form.Field>
<label>Age</label>
<input
name="age"
onChange={(e) => {
setAge(e.target.value);
handleChange(e);
}}
values={formValues.age}
/>
<p className="error-message">{formErrors.age}</p>
</Form.Field>
<Button type="submit">Submit</Button>
</Form>
</div>
);
I'm trying to use axios to do POST method for creating user.
I got everything works fine but there's one small problem but I don't know how to fix.
The problem is that I always need to submit the form 2 times to make the POST request. There's nothing happen in the first submit, but it will work in the second submit.
Does anyone know what's wrong with my code?
Edited
According to #DBS solution.
I'm trying to follow the steps but now the form can't submit anymore. Can someone let me know if I missed something?
const [name, setName] = useState("");
const [age, setAge] = useState("");
const initialValues = {
name: "",
age: "",
};
const [formValues, setFormValues] = useState(initialValues);
const [formErrors, setFormErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({ ...formValues, [name]: value });
};
const handleSubmit = (e) => {
if (!Object.keys(formErrors).length && !isSubmitting) {
setFormErrors(validate(formValues));
} else {
userCreate();
setisSubmitting(true);
}
};
const validate = (values) => {
const errors = {};
if (!values.name) {
errors.name = "Name is required";
}
if (!values.age) {
errors.age= "Age is required";
}
return errors;
};
const userCreate = async () => {
await api.post("/createuser", {
name,
age,
});
};
return (
<div class="container">
<Form
onSubmit={handleSubmit}
>
<Form.Field>
<label>Name</label>
<input
name="name"
onChange={(e) => {
setName(e.target.value);
handleChange(e);
}}
values={formValues.name}
/>
<span className="error-message">{formErrors.name}</span>
</Form.Field>
<Form.Field>
<label>Age</label>
<input
name="age"
onChange={(e) => {
setAge(e.target.value);
handleChange(e);
}}
values={formValues.age}
/>
<p className="error-message">{formErrors.age}</p>
<
The issue here is your isSubmit, it is required to be true for userCreate to be called:
onSubmit={
Object.keys(formErrors).length === 0 && isSubmit
? userCreate
: handleSubmit
}
But it starts as false:
const [isSubmit, setIsSubmit] = useState(false);
And is only ever updated when handleSubmit is called (which, confusingly, is only called when the validation fails)
So your current code does this:
isSubmit is false
Submit is clicked, handleSubmit is called and isSubmit is set to true
Submit is clicked again, now isSubmit is true it will call userCreate
To solve this, there are a few different approaches, but I would:
Move all the submit handler logic into onSubmit={handleSubmit} (To keep things clear)
Inside there, do your error length check (0 error) and isSubmit (Which I would probably rename to isSubmitting for clarity, and make sure it's false) (E.g. !Object.keys(formErrors).length && !isSubmitting)
If there are errors, show the appropriate message (Leaving isSubmitting as false)
If not, call userCreate (And set isSubmitting to true)
Lastly, if this can be submitted multiple times, add an effect/callback/then to reset isSubmitting once the call is complete.
Are you using the isSubmitting flag for something? if not below might be work for you.
If there is no error, calling the create method
const handleSubmit = (e) => {
setFormErrors(validate(formValues));
if(Object.keys(formErrors).length === 0) {
userCreate();
}
};
if isSubmitting is used to check the submit or create in progress
const handleSubmit = (e) => {
setFormErrors(validate(formValues));
if(Object.keys(formErrors).length === 0) {
setIsSubmitting(true);
userCreate();
}
};
The flag isSubmitting should be set as false on API is success or failed
setIsSubmitting(false)
I am pretty new to React, and the only CRUD functionality I have done has been in MVC.net, so I am pretty lost here and none of the tutorials I am finding seem to be the situation I have going on...or maybe I am just misunderstanding them. The tips I got with my initial question helped, but I am still no getting it to work, so hopefully now I have supplies enough info to help other help me. I did no include all of the input fields because their are like 15 and it was just redundant.
I am pulling up the modal using this onClick event:
onClick={()=> {handleEditModal(item)}}
modalFunctions.js
// Modal Functionality
export function ModalFunctions() {
const [selectedRecord, setSelectedRecord] = useState([]);
const [openModal, setOpenModal] = useState(false);
const handleEditModal = item =>{
setOpenModal(true)
setSelectedRecord(item)
}
return {
selectedRecord,
setSelectedRecord,
openModal,
setOpenModal,
handleEditModal,
handleDetailModal
}
}
// Form Functionality
export function FormFunctions(validateOnChange = false, validate) {
const [values, setValues] = useState('');
const [errors, setErrors] = useState({});
const handleInputChange = e => {
const { name, value } = e.target
setValues({
...values,
[name]: value
})
if (validateOnChange)
validate({ [name]: value })
}
return {errors, handleInputChange, setErrors, setValues, values}
}
DataTable.js
// Imported Modal Functions
const {
selectedRecord,
openModal,
setOpenModal,
handleEditModal,
handleDetailModal
} = ModalFunctions();
const baseURL = 'http://localhost:8080/api/tanks';
// Fetch Data
useEffect(() => {
const fetchData = async () =>{
setLoading(true);
try {
const {data: response} = await axios.get(baseURL);
setTableData(response);
} catch (error) {
console.error(error.message);
}
setLoading(false);
};
fetchData();
}, [baseURL, setTableData, setLoading]);
// The Modal
return(
<Modal
title={ "Editing: " + (selectedRecord.tankName) }
openModal={openModal}
setOpenModal={setOpenModal}
>
<TankEdit
selectedRecord={selectedRecord}
setOpenModal={setOpenModal}
openModal={openModal}
/>
</Modal>
)
TankEdit.js
export function TankEdit(props) {
const { baseURL, openModal, selectedRecord, setOpenModal, setTableData } = props;
const validate = (fieldValues = item) => {
let temp = { ...errors }
if ('tankName' in fieldValues)
temp.tankName = fieldValues.tankName ? "" : "This field is required."
setErrors({
...temp
})
if (fieldValues === values)
return Object.values(temp).every(x => x === " ")
}
const {
values,
setValues,
errors,
setErrors,
handleInputChange,
} = FormFunctions(true, validate);
useEffect(() => {
if (selectedRecord !== null)
setValues({
...selectedRecord
})
}, [selectedRecord, setValues])
function editRecord() {
axios.put(`${baseURL}`, {
title: "Success",
body: "The record had been successfully updated"
}).then ((response) => {setTableData(response.data);})
}
const handleSubmit = e => {
e.preventDefault()
if (validate()) {
editRecord(values);
}
setOpenModal(false)
}
const item = values; // used for easier referencing (matches table item)
return (
<Form onSubmit={handleSubmit} open={openModal}>
<Grid>
<Controls.Input
name="tankName"
label="Tank Name"
value={item.tankName}
onChange={handleInputChange}
error={errors.tankName}
/>
</Grid>
<Grid>
<Controls.Button
type="submit"
text="Submit"
/>
<Controls.Button
text="Cancel"
color="default"
onClick={()=>{setOpenModal(false)}}
/>
</Grid>
</Form>
)
}
Input.js
export default function Input(props) {
const { error=null, label, name, onChange, value, ...other } = props;
return (
<TextField
variant="outlined"
label={label}
name={name}
value={value}
defaultValue=''
onChange={onChange}
{...other}
{...(error && {error:true,helperText:error})}
/>
)
}
My company is only wanting a Read and an Update function, since Creating and Deletion will be handled another ways, so this is my final hangup. I think I am close, but I am missing something.
Can anyone point me in the right direction?
THANKS!!!!
If you want to write an update request you would use axios.put to send the data to your back-end.
In your handleSubmit function you do:
let response = await axios.put('http://-------/api/tanks', { name: 'tank' });
(The second parameter is an object that needs to contain all the form data fields)
Also make sure you call e.preventDefault() in the handleSubmit function so you don't accidentally navigate elsewhere.
Then you will update your database or whatever using your back-end.
for update you should use put or patch method
and you should send id of item you want to update in request url.
I insert 2 example here.
this is for put:
const res = await axios.put('/api/article/123', {
title: 'Making PUT Requests with Axios',
status: 'published'
});
this is for patch:
const res = await axios.patch('/api/article/123', {
title: 'Making PUT Requests with Axios',
status: 'published'
});
I created an input field which am trying to validate
const [name, setName] = useState('');
const [formErrors, setFormErrors] = useState({});
<p>Name</p>
<input
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Error> {formErrors.name}</Error>
<Button
onClick={handleSubmit}
>
Submit
</Button>
OnClick of the submit button it checks if the name field is empty in the handleSubmit function
const validate = (values) => {
const errors = {};
if (!values.name) {
errors.name = 'Name is required!';
}
return errors;
};
const handleSubmit = async (e) => {
const val = {name};
setFormErrors(validate(val));
if (Object.keys(formErrors).length === 0) {
console.log('No empty');
}else{
console.log('Empty');
}
};
The issue am having is that it lags behind in response. For example if the name field is empty it console's Not empty, on first click of the buttton, if I then click the button again it then console's the correct data which is 'Empty'.
This is becasue the state is not set until the component is being re-render therefore the formErrors state is {} until the handle function ends. Create a new constant to hold the errors and use these to console the outcome instead of the state itself if you still need to do something during the event, however use the state inside the JSX to render correctly since state will have been changed by then.
const handleSubmit = async (e)=>{
const val = {name};
const errors = validate(val)
setFormErrors(errors);
if (Object.keys(errors).length === 0) {
console.log('No empty');
}else{
console.log('Empty');
}
}
Isn't the hook useCallback supposed to return an updated function every time a dependency change?
I wrote this code sandbox trying to reduce the problem I'm facing in my real app to the minimum reproducible example.
import { useCallback, useState } from "react";
const fields = [
{
name: "first_name",
onSubmitTransformer: (x) => "",
defaultValue: ""
},
{
name: "last_name",
onSubmitTransformer: (x) => x.replace("0", ""),
defaultValue: ""
}
];
export default function App() {
const [instance, setInstance] = useState(
fields.reduce(
(acc, { name, defaultValue }) => ({ ...acc, [name]: defaultValue }),
{}
)
);
const onChange = (name, e) =>
setInstance((instance) => ({ ...instance, [name]: e.target.value }));
const validate = useCallback(() => {
Object.entries(instance).forEach(([k, v]) => {
if (v === "") {
console.log("error while validating", k, "value cannot be empty");
}
});
}, [instance]);
const onSubmit = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
setInstance((instance) =>
fields.reduce(
(acc, { name, onSubmitTransformer }) => ({
...acc,
[name]: onSubmitTransformer(acc[name])
}),
instance
)
);
validate();
},
[validate]
);
return (
<div className="App">
<form onSubmit={onSubmit}>
{fields.map(({ name }) => (
<input
key={`field_${name}`}
placeholder={name}
value={instance[name]}
onChange={(e) => onChange(name, e)}
/>
))}
<button type="submit">Create object</button>
</form>
</div>
);
}
This is my code. Basically it renders a form based on fields. Fields is a list of objects containing characteristics of the field. Among characteristic there one called onSubmitTransformer that is applied when user submit the form. When user submit the form after tranforming values, a validation is performed. I wrapped validate inside a useCallback hook because it uses instance value that is changed right before by transform function.
To test the code sandbox example please type something is first_name input field and submit.
Expected behaviour would be to see in the console the error log statement for first_name as transformer is going to change it to ''.
Problem is validate seems to not update properly.
This seems like an issue with understanding how React lifecycle works. Calling setInstance will not update instance immediately, instead instance will be updated on the next render. Similarly, validate will not update until the next render. So within your onSubmit function, you trigger a rerender by calling setInstance, but then run validate using the value of instance at the beginning of this render (before the onSubmitTransformer functions have run).
A simple way to fix this is to refactor validate so that it accepts a value for instance instead of using the one from state directly. Then transform the values on instance outside of setInstance.
Here's an example:
function App() {
// setup
const validate = useCallback((instance) => {
// validate as usual
}, []);
const onSubmit = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const transformedInstance = fields.reduce((acc, {name, onSubmitTransformer}) => ({
...acc,
[name]: onSubmitTransformer(acc[name]),
}), instance);
setInstance(transformedInstance);
validate(transformedInstance);
}, [instance, validate]);
// rest of component
}
Now the only worry might be using a stale version of instance (which could happen if instance is updated and onSubmit is called in the same render). If you're concerned about this, you could add a ref value for instance and use that for submission and validation. This way would be a bit closer to your current code.
Here's an alternate example using that approach:
function App() {
const [instance, setInstance] = useState(/* ... */);
const instanceRef = useRef(instance);
useEffect(() => {
instanceRef.current = instance;
}, [instance]);
const validate = useCallback(() => {
Object.entries(instanceRef.current).forEach(([k, v]) => {
if (v === "") {
console.log("error while validating", k, "value cannot be empty");
}
});
}, []);
const onSubmit = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const transformedInstance = fields.reduce((acc, {name, onSubmitTransformer}) => ({
...acc,
[name]: onSubmitTransformer(acc[name]),
}), instanceRef.current);
setInstance(transformedInstance);
validate(transformedInstance);
}, [validate]);
// rest of component
}
Hi there i'm new to react i'm setting state and city using pincode when it has length of 6 digits when i put whole function inside useEffect it gives error to include setPersonalDetState & i also want to use same funtion to validate i cannot include it inside useEffect
const intialState = {
city: '',
state: '',
pincode: ''
};
const PersonalDetails = () => {
const [personalDetState, setPersonalDetState] = useState(intialState);
const { city, state, pincode } = personalDetState;
const fetchPincode = async (pincode) => {
if (pincode.length != 6) {
return;
}
let cityState = await axios.get(
`https://api.postalpincode.in/pincode/${pincode}`
);
const { Status, PostOffice } = cityState.data[0];
const { District, State } = PostOffice[0];
personalDetState['city'] = District;
personalDetState['state'] = State;
return setPersonalDetState({ ...personalDetState });
};
useEffect(() => {
fetchPincode(pincode);
}, [pincode]);
const handleChange = (event) => {
let { value, name } = event.target;
if (name === 'pincode') value = value.replace(/\D/g, '');
if (name === 'pincode' && value.length === 7) return;
setPersonalDetState({ ...personalDetState, [name]: value });
};
return (
<Fragment>
<input
type='text'
name='pincode'
value={pincode}
onChange={handleChange}
/>
<input type='text' name='city' value={city} disabled />
<input type='text' name='state' value={state} disabled />
</Fragment>
);
};
You can useCallback for fetchPincode in order to correctly specify it as a dependency to the effect. It will run whenever pincode changes, but since fetchPincode callback doesn't have dependencies, it won't trigger the effect.
const intialState = {
city: '',
state: '',
pincode: ''
};
const PersonalDetails = () => {
const [personalDetState, setPersonalDetState] = useState(intialState);
const { city, state, pincode } = personalDetState;
const fetchPincode = useCallback(() => {
if (pincode.length != 6) {
return;
}
axios.get(
`https://api.postalpincode.in/pincode/${pincode}`
).then(cityState => {
const { Status, PostOffice } = cityState.data[0];
const { District, State } = PostOffice[0];
setPersonalDetState(prev => ({
city: District,
state: State
}));
});
}, [pincode]);
useEffect(fetchPincode, [pincode]);
const handleChange = (event) => {
let { value, name } = event.target;
if (name === 'pincode') value = value.replace(/\D/g, '');
if (name === 'pincode' && value.length === 7) return;
setCredentials({ ...userCredentials, [name]: value });
};
return (
<Fragment>
<input
type='text'
name='pincode'
value={pincode}
onChange={handleChange}
/>
<input type='text' name='city' value={city} disabled />
<input type='text' name='state' value={state} disabled />
</Fragment>
);
};
I've rewritten the fetchPincode to use the old-fashion Promise syntax, because it must not return anything in order to be used as useEffect callback. I answered a similar question here.
Why useCallback? In order to keep the same function reference between re-renders. Reference to the function will change if its dependencies change (dependencies are specified as array right after the function, as second parameter to useCallback).
Why the same function reference? Because if included as a dependency to another hook (say useEffect), changing reference may cause the effect to re-run (in case of effects), which is probably not desired if that function reference changes too often.
In your particular case, pincode will change whenever you type (not 100% sure, but I assume setCredentials is doing that, I didn't see its declaration). This will cause the function reference to fetchPincode to change. If specified as a dependency to an effect, the effect would run if the function ref changes. But you also have pincode specified as dependency so effect would run on each key input anyway. But you might think about making the request after specified period of inactivity (user not typing), also known as debounce.
To learn about hooks:
Introducing hooks
Hooks FAQ