I'm trying to make a custom reusable useForm hook with inputs validation. I want form to be validated on Submit and clear validation when user typing something in the field. Actually the problem with validation. I do not understand how to implement it.
Hook accepts inital state for inputs which is an object with keys and values as empty strings:
{
"name": "",
"description": ""
}
Hooks also accept a callback function which is actually a submitHandler which fires on submit click but do nothing at the moment.
I have a submit function in useForm hook which expected to fire when a form is valid. To check the form validity I loop through inputs calling validator function on each input. If input is not valid I set an error for that field.
But its not clear what to do next. If I console.log(error) in submit function right after forEach loop after first submit button click its an empty object (on second button click it has errors but not for the first click).
May I ask for help tp deal with problem? How to make this validation work correctly?
Submit function in useForm hook:
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
Object.entries(input).forEach(([field, value]) => {
validator(field, value)
})
console.log(error) // it's an empty object on first submit button click
callback();
};
Validator function:
const validator = (field: keyof Error, value: Input['key']) => {
switch (field) {
case 'name':
if (value.length <= 2) {
setErrors((prev) => ({
...prev,
[field]: 'Min 2 characters for name field',
}));
}
break;
case 'description':
if (value.length <= 2) {
setErrors((prev) => ({
...prev,
[field]: 'Min 2 characters for description field',
}));
}
break;
default:
break;
}
};
Full hook code:
import { useState } from 'react';
type Input = {
[key: string]: string;
};
type Error = {
[key: string]: string | undefined;
};
const useForm = (initValue: Input, callback: () => void) => {
const [input, setInput] = useState(initValue);
const [errors, setErrors] = useState<Error>({});
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
clearValidation(name);
setInput((prevState) => ({
...prevState,
[name]: value,
}));
};
const resetForm = () => {
setInput({});
};
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
Object.entries(input).forEach(([field, value]) => {
validator(field, value);
});
callback();
};
const validator = (field: keyof Error, value: Input['key']) => {
switch (field) {
case 'name':
if (value.length <= 2) {
setErrors((prev) => ({
...prev,
[field]: 'Min 2 characters for name field',
}));
}
break;
case 'description':
if (value.length <= 2) {
setErrors((prev) => ({
...prev,
[field]: 'Min 2 characters for description field',
}));
}
break;
default:
break;
}
};
const clearValidation = (field: keyof Error) => {
if (field) {
setErrors((prevState) => ({
...prevState,
[field]: undefined,
}));
return;
}
setErrors({});
};
return { input, errors, setInput, handleChange, resetForm, submit };
};
export default useForm;
The reason the console.log is empty is because react state setting is asynchronous. It's probably setting it fine, it just happens shortly after the console.log because of how React batches state updates and runs them later seamlessly.
I would suggest logging it in the render method or something.
See here for more details: https://beta.reactjs.org/apis/usestate#ive-updated-the-state-but-logging-gives-me-the-old-value
Related
The webpage was being displayed prior to adding the firebase setDoc code. I have excluded the render tag, because when it is included I when adding in a render tag I am given an error stating "cannot be used as a JSX component. Its return type 'void' is not a valid JSX element." This code is sending an option selected by a user using a radio button to firebase. Thanks in advance.
function RoleDecision(this: any) {
let { user } = useUser();
let { firestore } = useFirebase();
let onValueChange = async (e: { target: { value: any } }) => {
this.setState({
selectedOption: e.target.value,
});
};
let formSubmit = async (e: { preventDefault: () => void }) => {
e.preventDefault();
console.log(this.state.selectedOption);
};
useEffect(() => {
if (user && firestore) {
(async () => {
setDoc(doc(firestore!, "users", `${user?.uid}`), {
role: this.state.selectedOption,
})
.then(() => {
console.log("Document successfully written!");
})
.catch((error) => {
console.error("Error writing document: ", error);
});
})();
}
}, [firestore]);
You can not use React class based and function based components simultaneously in the same file/component!
My suggestion is to use function based only components, and to replace the this.state usage with useState() hook, the onSubmit and onChange events with proper handler functions like, const onValueChange = (...) => {...}
I'm creating a form that can get time value of time picker.
But due to the e is already the Moment, I cant get the input name in handleChange.
Is there a way to get it?
Component:
<DateTimeRangePicker
selected={time}
onChange={handleChange}
type='timepicker'
readOnly={false}
texts={{
name: 'time',
placeholder: 'Enter your time out'
}}
timeFormat='HH:MM:SS'
/>
the texts?.name will be a props and will be inside of innerProps of DatetimepickerProps
CustomForm handleChange
const [values, setValues] = useState(initialState)
const [data, setData] = useState(initialState)
const handleChange = useCallback(
(e: any) => {
let result: any
setValues({ ...values, [e.target.name]: e })
if (e._isValid === true) {
result = {
value: e._d,
status: true
}
} else {
result = {
value: e._d,
status: false
}
}
setData({ ...data, [e.target.name]: result })
},
[data]
)
It has to be e.target.name, since the CustomForm onSubmit will get the other input component throught its e.target.name
For the component DateTimeRangePicker from react-datetime-picker
What you receive from the picker onChange, is not the typical event that you would get from a Jsx element; rather it is the time value of the picker
You can see it in the source code via this link
A solution would be to wrap your handleChange and use a constant to define the name of your property as follow
const TIME_NAME = "time";
const YourComponent = () => {
const [values, setValues] = useState(initialState)
const [data, setData] = useState(initialState)
const handleChange = useCallback(
(e: any) => {
let result: any
setValues({ ...values, [e.target.name]: e.target.value })
if (e._isValid === true) {
result = {
value: e.target.value,
status: true
}
} else {
result = {
value: e.target.value,
status: false
}
}
setData({ ...data, [e.target.name]: result })
},
[data]
);
const handleDateTimeRangePickerChange = (_value) => {
handleChange({target: {name: TIME_NAME, value: _value }});
}
return <DateTimeRangePicker
name={TIME_NAME}
selected={time}
onChange={handleDateTimeRangePickerChange}
type='timepicker'
readOnly={false}
texts={{
name: 'time',
placeholder: 'Enter your time out'
}}
timeFormat='HH:MM:SS'
/>
}
I'm new to react-admin and I am trying to build a custom image gallery input. it should show a modal with images (data is already fetched and stored in the redux) so the user can select one or more images (upon selection an action is dispatched to update the reducer's value) and I need these selected images ids in the transform function on <Create /> so I can add the required data before dataProvider method is called.
but I have a weird issue, that might be because of my lack of react knowledge. in the snippet below, I try to get the useReducers value and then add it to the form.
import React, { useReducer, useMemo, useEffect, useCallback } from 'react';
import { Create as Ra_create } from 'react-admin';
const ctxInitialValues = {};
const galleryCtx = React.createContext(ctxInitialValues);
const CreateWithGallery = (props) => {
const [selectedImages, dispatch] = useReducer((state, { type, payload }) => {
switch (type) {
case 'UPDATE_STATE':
return { ...payload };
case 'INIT_RECORD':
return {
...state,
[payload]: [],
};
default:
return state;
}
}, ctxInitialValues);
const updateSelection = (record, image, operation) => {
if (operation === 'add') {
let newState = {
...selectedImages,
[record]: [...selectedImages[record], image],
};
dispatch({
type: 'UPDATE_STATE',
payload: newState,
});
} else if (operation === 'remove') {
let newState = {
...selectedImages,
[record]: selectedImages[record].filter((item) => item.id !== image.id),
};
dispatch({
type: 'UPDATE_STATE',
payload: newState,
});
}
};
const transformPayload = (data) => {
let transformed = {
...data,
};
// but I get {} here
for (let record in selectedImages) {
transformed[record] = selectedImages[record].map((item) => ({
id: item.id,
}));
}
return transformed;
};
useEffect(() => {
console.log(selectedImages);
// I get fresh values here
}, [selectedImages]);
const initializeRecord = (record) => {
dispatch({
type: 'INIT_RECORD',
payload: record,
});
};
return (
<galleryCtx.Provider
value={{
selectedImages,
updateSelection,
initializeRecord,
}}
>
<Ra_create {...props} transform={transformPayload}>
{props.children}
</Ra_create>
</galleryCtx.Provider>
);
};
export { galleryCtx };
export default CreateWithGallery;
when I try to access the selectedImages values in the transform function I get {}, which is the initial state. I have tried using useCallback and useMemo to make sure the values are changed after each dispatch but it did not make any difference.
there's also a similar behavior in this question as well:
React Admin: how to pass state to transform
how can I use state in the transform function?
I ended up with setting the transform prop on the component (in custom toolbar):
const CustomToolbar = (props: any) => {
const transform = useCallback((data: any) => {
return {
...data,
files: something_from_state,
};
}, [something_from_state]);
const handleClick = () => {
};
return <Toolbar {...props}>
<SaveButton
handleSubmitWithRedirect={handleClick} transform={transform}/>
</Toolbar>
};
to fix this you can use transform prop on as explained in the react-admin docs. it is still unclear though, why we can't get state in the transform function on the or .
I built a custom hook to handle a form. One thing I'm having trouble with is calling the validation while the input value is changing.
I have four code snippets included. The second and third are just for context to show how the complete custom hook but feel free to skip them as I'm just curious about how to implement similar functionality from snippet 1 in snippet 4.
The reason I want to do this, in addition to calling it on submit, is that if the input value becomes ' ' I would like to display the error message and when a user started typing it would go away.
This was pretty simple when I wasn't using hooks I would just call a validate function after setState like this:
const validate = (name) => {
switch(name):
case "username":
if(!values.username) {
errors.username = "What's your username?";
}
break;
default:
if(!values.username) {
errors.username = "What's your username?";
}
if(!values.password) {
errors.username = "What's your password?";
}
break;
}
const handleChange = (e) => {
let { name, value } = e.target;
this.setState({ ...values,
[name]: value
}, () => this.validate(name))
}
So now using react hooks things are not as easy. I created a custom form handler that returns values, errors, handleChange, and handleSubmit. The form handler is passed an initialState, validate function, and a callback. As of now it looks like this:
import useForm from './form.handler.js';
import validate from './form.validation.js';
const schema = { username: "", password: "" }
export default function Form() {
const { values, errors, handleChange, handleSubmit } = useForm(schema, validate, submit);
function submit() {
console.log('submit:', values);
}
return (
<form></form> // form stuff
)
}
Here's the validation file. It's simple, it just requires values for two fields.
export default function validate(values) {
let errors = {};
if(!values.username) {
errors.username = "What's your username?";
}
if(!values.password) {
errors.password = "What's your password?";
}
return errors;
}
Now here is my form handler, where I'm trying to solve this problem. I have been trying different things around calling setErrors(validate(values)) in the useEffect but can't access the input. I'm not sure, but currently, the custom hook looks like this:
import { useState, useEffect, useCallback } from 'react';
export default function useForm(schema, validate, callback) {
const [values, setValues] = useState(schema),
[errors, setErrors] = useState({}),
[loading, setLoading] = useState(false); // true when form is submitting
useEffect(() => {
if(Object.keys(errors).length === 0 && loading) {
callback();
}
setLoading(false);
}, [errors, loading, callback])
// I see useCallback used for event handler's. Not part of my questions, but is it to prevent possible memory leak?
const handleChange = (e) => {
let { name, value } = e.target;
setValues({ ...values, [name]: value });
}
const handleSubmit = (e) => {
e.preventDefault();
setLoading(true);
setErrors(validate(values));
}
return { values, errors, handleChange, handleSubmit }
}
I'm not sure if it's a good idea to set other state (errors) while in a callback to set state (values) so created a code review
As commented; you can set errors while setting values:
const Component = () => {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const onChange = useCallback(
(name, value) =>
setValues((values) => {
const newValues = { ...values, [name]: value };
setErrors(validate(newValues));//set other state while in a callback
return newValues;
}),
[]
);
return <jsx />;
};
Or combine values and errors:
const Component = () => {
const [form, setForm] = useState({
values: {},
errors: {},
});
const onChange = useCallback(
(name, value) =>
setForm((form) => {
const values = { ...form.values, [name]: value };
const errors = validate(values);
return { values, errors };
}),
[]
);
const { errors, values } = form;
return <jsx />;
};
I am new to React, I am trying to implement some error validation with a react arrow function but got no luck all day.
The catch works and I can print the errors, but I dont know how to link the errors I am printing inside errorsHandler() to the other const where the form is for styling and warnings.
const errorsHandler= fieldErrors => {
if (fieldErrors) {
fieldErrors.forEach(err => {
const errorFieldName= err.field;
const errorDescription = err.message;
console.log('Field', errorFieldName, 'Description', errorDescription);
// Field name Description name already exists
});
}
};
export const defaultGroupModel = {
description: '',
active: true,
name: '',
}
const GroupFormModal = ({ editId, editGroup, onSave}) => {
const [groupState, setGroupState] = useState(defaultGroupModel);
useEffect(() => {
if (editGroup) {
setGroupState(editGroup);
}
}, [editGroup]);
const handleChange = ({ target: { value, name} }) => {
setGroupState({ ...groupState, [name]: value });
};
return ( (...) <Form onSubmit={e => onSave(e, groupState)} onReset={onReset}> (...);
};
const mapDispatchToProps = dispatch => ({
onSave: (e, group) => {
e.preventDefault();
if (group.id) {
dispatch(updateGroup(group));
} else {
dispatch(createGroup(group)).catch(error => {
errorsHandler(error.data.fieldErrors);
});
}
},
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(GroupFormModal);
I have tried to create an [errorState, setErrorState] and use useEffect inside the errorsHandler but get Invalid Hook. How can I have the handle inside the catch to be in the same context as the form ?
Thank you in advance
There are few things you can do here. First, use mapDispatchToProps to wrap inside dispatch your action's creators (without then and catch)
const mapDispatchToProps = dispatch =>({
updateGroup : group => dispatch(updateGroup(group)),
createGroup : group => dispatch(createGroup(group))
})
Now you can set an internal state to reflect those errors
const Component = ({ updateGroup, createGroup }) =>{
const [errors, setErrors] = useState(false)
const onSave = (group,e) =>{
createGroup(group)
.then(res => console.log('everything ok'))
.catch(err => setError(err) /* now you have the errors inside your component*/)
}
return <form onSubmit={ e => onSave(group,e)) }> /*...*/ </form>
}