Re-render problem
I have two components:
MainForm
CFNumericInput
The values coming from CFNumericInput are correct, but setValue won't render the old one.
Do I have to use an useEffect?
MainForm
const { control, watch, setValue, register } = useFormContext();
return (
<CFNumericInput
name="number1"
control={control}
setValueOnChange={(nextValue: string, oldValue: string) => {
let nextValidValue = checkNumericType(nextValue, "f6.2");
if (nextValidValue !== "") setValue("number1", nextValidValue);
else if (oldValue) setValue("number1", oldValue);
}}
/>;
)
CFNumericInput
export const CFNumericInput: React.FC<any> = ({
name,
control,
setValueOnChange,
}) => {
return control ? (
<Controller
name={name}
control={control}
render={({ field }) => {
return (
<NumericInput
{...field} // onChange, onBlur, value, name, ref
title={title}
onChange={(e) => {
field.onChange(e);
setValueOnChange && setValueOnChange(e, field.value);
}}
/>
);
}}
/>
) : (
<></>
);
};
Working but heavy solution
This solution it's working, but it's really heavy.
const [number1] = watch(["number1"]);
const [old, setOld] = useState("");
useEffect(() => {
let nextValidValue = checkNumericType(number1, "f6.2");
if (nextValidValue !== "") {
setValue("number1", nextValidValue);
setOld(nextValidValue);
} else if (old) setValue("number1", old);
}, [number1]);
Isn't possible that you call checkNumericType on onSubmit?
Also you can try on looking to use Yup (take a look at: https://react-hook-form.com/advanced-usage/#CustomHookwithResolver)
let me know what have you tried
I have search component with validate js.
Problem: when my input in foucs first time, validate and request dont work, but when i lose focus my input, and click it again, and try again, search working without validation
interface IProps {
onSearchChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
const Search: React.FC<IProps> = ({ onSearchChange }) => {
const inputRef = useRef<HTMLInputElement>(null);
const [inputIsTouched, setInputIsTouched] = useState(false);
const currentValue = inputRef.current?.value && inputRef.current.value;
const validateErrors = validate({ currentValue }, constraints);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (validateErrors?.currentValue) {
return;
}
currentValue && onSearchChange && onSearchChange(event);
setInputIsTouched(true);
};
const debouncedOnChange = debounce(handleChange, 1000);
return (
<div className={classes['Root']}>
<Input
type="text"
autoComplete="off"
placeholder="..."
onChange={debouncedOnChange}
ref={inputRef}
onBlur={() => setInputIsTouched(true)}
isError={inputIsTouched && !!validateErrors?.currentValue}
/>
<div className={classes['ErrorContainer']}>
{inputIsTouched && validateErrors?.currentValue && (
<Text color="error" size="s">
{validateErrors.currentValue}
</Text>
)}
</div>
</div>
);
};
That's expected because on first render, currentValue is undefined (as inputRef.current is null) and there's nothing calling handleChange to trigger the search.
You need to make sure the handleChange logic also runs on the initial render, so it should look something like this:
const Search: React.FC<IProps> = ({ onSearchChange }) => {
// Use a single object for all input state props:
const [{
isTouched,
validateErrors,
}, setInputState] = useState({
isTouched: false,
validateErrors: null,
});
const inputRef = useRef<HTMLInputElement>(null);
// Debounce only search callback:
const debouncedSearchChange = debounce(onSearchChange, 1000);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
// Get the current value:
const currentValue = e.currentTarget.value;
// Validate it:
const validateErrors = validate({ currentValue }, constraints);
if (validateErrors?.currentValue) {
// And handle error:
setInputState(prevState => ({ ...prevState, validateErrors }));
return;
}
// Or success:
setInputState(prevState => ({ ...prevState, validateErrors: null }));
// And trigger the debounced search if needed:
if (currentValue && debouncedSearchChange ) debouncedSearchChange(event);
}, [constraints, debouncedSearchChange]);
// Trigger validation and search on first render:
useEffect(() => {
const inputElement = inputRef.current;
// TypeScript will complain about this line, so you might want to
// re-structure the logic above to accommodate this:
if (inputElement) handleChange({ currentTarget: inputElement });
}, []);
return (
<div className={classes['Root']}>
<Input
type="text"
autoComplete="off"
placeholder="..."
onChange={handleChange}
ref={inputRef}
onBlur={() => setInputState(prevState => ({ ...prevState, isTouched: true }))}
isError={inputIsTouched && !!validateErrors?.currentValue}
/>
<div className={classes['ErrorContainer']}>
{inputIsTouched && validateErrors?.currentValue && (
<Text color="error" size="s">
{validateErrors.currentValue}
</Text>
)}
</div>
</div>
);
};
working on an issue with the react-select AsyncSelect component that loads options from an API.But I can't pass the information to React-Hook Form through the controller.AsyncSelect works perfectly. The data goes back well in my "SelelectedValue" state. Can anyone help me ?
const [inputValue, setValue] = useState('');
const [selectedValue, setSelectedValue] = useState(null);
// handle input change event
const handleInputChange = value => {
setValue(value);
};
// handle selection
const handleChange = value => {
setSelectedValue(value);
}
const loadOptions = async (inputValue, callback) => {
const response = await fetch(`APIurl`);
const json = await response.json();
const object = json.records;
callback(object.map(i => ({ label: `${i.fields.firstName} - ${i.fields.lasName} , value: i.fields.firstName })))
}
<Controller
name="company"
control={control}
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<AsyncSelect
isClearable
value={selectedValue}
placeholder={'Your information'}
loadOptions={loadOptions}
onInputChange={handleInputChange}
onChange={handleChange}
styles={customStyles}
/>)}
/>
react-hook-form manages some common event and state (like value, onChange, onBlur etc.) for you so there is no need to define your own state in most case except onInputChange in AsyncSelect.
You can try to select the option and submit the form.
<Controller
name="company"
control={control}
rules={{ required: true }}
render={({ field }) => (
<AsyncSelect
{...field}
isClearable
defaultOptions
placeholder={"Your information"}
loadOptions={loadOptions}
onInputChange={handleInputChange}
// styles={customStyles}
/>
)}
/>
Here is the codesandbox
I have a react native input component that takes a regex and emit a boolean if the regex matches.
I wanted to do the similar thing but return a string value in React Js but the problem is I don't totally understand how the react native input component is working.
Custom Input component
class CustomInput extends Component {
handleValidation(value) {
const {pattern} = this.props;
if (!pattern) {
return true;
}
// string pattern, one validation rule
if (typeof pattern === 'string') {
const condition = new RegExp(pattern, 'g');
return condition.test(value);
}
// array patterns, multiple validation rules
if (typeof pattern === 'object') {
const conditions = pattern.map(rule => new RegExp(rule, 'g'));
return conditions.map(condition => condition.test(value));
}
}
onChange(value) {
const {onChangeText, onValidation} = this.props;
const isValid = this.handleValidation(value);
onValidation && onValidation(isValid);
onChangeText && onChangeText(value);
}
render() {
const {pattern, onChangeText, children, style, ...props} = this.props;
return (
<Input
style={style}
onChangeText={value => this.onChange(value)}
{...props}
autoCapitalize="none">
{children}
</Input>
);
}
}
Usage
<CustomInput
value={pass} //state variable
onChangeText={onChangePass} //state variable setter
pattern={[regexes.password]}
onValidation={isValid => performValidation(isValid)}
//performValidatio method enables and disables a button according to isValid
/>
Now I want to make a component in ReactJs that has an Input and other elements the input will take a regex and and it will return the value of its input to the parent. It will be used in a form that will have many inputs and all inputs need to have and error msg and validations.
You can create custom component with controlled state. You need to set local state within the custom component. On change of value, you can parse and validate. after that set it to local state. You can leverage error either local of delegate to parent. Here below sample, i have delegated to parent on validation.
const Input = ({
onChangeText,
pattern,
onValidation,
defaultValue,
error,
...rest
}) => {
const [value, setValue] = useValue(defaultValue);
const onChange = ({ target: { value } }) => {
if (pattern) onValidation(pattern.test(value));
setValue(value);
onChangeText(value);
};
return (
<div className="wrapper">
<input value={value} onChange={onChange} {...rest} />
{error && <span>{error}</span>}
</div>
);
};
I have understood the logic if anyone wants to make an input like this they can use this code
I have used react-bootstrap for styling
The Custom Input
import React, { useState } from "react";
export default function CustomInput(props) {
const { error, inputProps, regex, className, onInput } = props;
const [showError, setShowError] = useState(false);
const [text, setText] = useState("");
const handleChange = (val) => {
setText(val);
if (regex.test(val)) {
setShowError(false);
} else {
setShowError(true);
}
onInput && onInput(val);
};
return (
<div className={className}>
<input
value={text}
className={showError ? "form-control border-danger" : "form-control"}
{...inputProps}
onChange={(e) => handleChange(e.target.value)}
/>
{showError && error ? (
<small className="text-danger">{error}</small>
) : null}
</div>
);
}
And use it in a parent component like this
const [temp, setTemp] = useState("");
<CustomInput
className="form-group col-md-3"
inputProps={{ placeholder: "test", maxLength: "50" }}
error="Required"
regex={regexes.url}
onInput={(val) => setTemp(val)}
/>
Somebody please confirm if this is a good approach
My Component has form input fields. These made use of a useState hook with their value and setValue for each input field. I want to optimize my component so the input fields made use of the same custom Hook which I called useFormInput
Inspired by Dan Abramov https://youtu.be/dpw9EHDh2bM see at 49:42
This works perfectly. However now I want to update the username after a new exercise is created. This is in the onSubmit method. But I'm not sure how to do this. Before I refactored I could use setUserName(), but now username is set by the generic custom hook function useFormInput
the username has an onChange method, so I thought I can maybe use this. However this uses the e.target.value because it is used for an input field.
Component:
I commented out the setUserName(''), here I want to update the username
const CreateExercise = () => {
const inputEl = useRef(null)
const username = useFormInput('')
const description = useFormInput('')
const duration = useFormInput(0)
const date = useFormInput(new Date())
const [users, setUsers] = useState([])
useEffect(() => {
axios
.get('http://localhost:5000/users/')
.then(res => {
if (res.data.length > 0) {
setUsers(res.data.map(user => user.username))
}
})
.catch(err => console.log(err))
}, [])
const onSubmit = e => {
e.preventDefault()
const exercise = {
username: username.value,
description: description.value,
duration: duration.value,
date: date.value
}
axios
.post('http://localhost:5000/exercises/add', exercise)
.then(res => console.log(res.data))
debugger
// setUsername('')
window.location = '/'
}
custom Hook useFormInput:
const useFormInput = initialValue => {
const [value, setValue] = useState(initialValue)
const handleChange = e => {
const newValue = e.target ? e.target.value : e
setValue(newValue)
}
return {
value,
onChange: handleChange
}
}
I expect the value in the state of username is updated to an empty string ' '
Complete code is on my repo on https://github.com/jeltehomminga/mern-tracker
Instead of trying to maintain more than 1 state, I'd recommend combining all state into one object. Then you can move everything into your custom hook. In addition, always make sure you handle and communicate any errors to the user.
Working example:
State as an object
hooks/useFormHandler (the API defined below is an object with functions to mimic API calls -- you'll replace this with real API calls. Also, if you wanted to make this hook reusable for other form components, then you'll need to remove the useEffect and handleSubmit functions from the custom hook and place them inside the specified functional component instead)
import { useCallback, useEffect, useState } from "react";
import API from "../../API";
// create a custom useFormHandler hook that returns initial values,
// a handleChange function to update the field values and a handleSubmit
// function to handle form submissions.
const useFormHandler = initialState => {
const [values, setValues] = useState(initialState);
// on initial load this will attempt to fetch users and set them to state
// otherwise, if it fails, it'll set an error to state.
useEffect(() => {
API.get("http://localhost:5000/users/")
.then(res => {
if (res.data.length > 0) {
setValues(prevState => ({
...prevState,
users: res.data.map(({ username }) => username)
}));
} else {
setValues(prevState => ({
...prevState,
error: "Unable to locate users."
}));
}
})
.catch(err =>
setValues(prevState => ({ ...prevState, error: err.toString() }))
);
}, []);
// the handleChange function will first deconstruct e.target.name and
// e.target.value, then in the setValues callback function, it'll
// spread out any previous state before updating the changed field via
// [name] (e.target.name) and updating it with "value" (e.target.value)
const handleChange = useCallback(
({ target: { name, value } }) =>
setValues(prevState => ({ ...prevState, error: "", [name]: value })),
[]
);
// the handleSubmit function will send a request to the API, if it
// succeeds, it'll print a message and reset the form values, otherwise,
// if it fails, it'll set an error to state.
const handleSubmit = useCallback(
e => {
e.preventDefault();
const exercise = {
username: values.username,
description: values.description,
duration: values.duration,
date: values.date
};
// if any fields are empty, display an error
const emptyFields = Object.keys(exercise).some(field => !values[field]);
if (emptyFields) {
setValues(prevState => ({
...prevState,
error: "Please fill out all fields!"
}));
return;
}
API.post("http://localhost:5000/exercises/add", exercise)
.then(res => {
alert(JSON.stringify(res.message, null, 4));
setValues(prevState => ({ ...prevState, ...initialState }));
})
.catch(err =>
setValues(prevState => ({ ...prevState, error: err.toString() }))
);
},
[initialState, setValues, values]
);
return {
handleChange,
handleSubmit,
values
};
};
export default useFormHandler;
components/CreateExerciseForm
import isEmpty from "lodash/isEmpty";
import React, { Fragment } from "react";
import { FaCalendarPlus } from "react-icons/fa";
import Spinner from "react-spinkit";
import Button from "../Button";
import Input from "../Input";
import Select from "../Select";
import useFormHandler from "../../hooks/useFormHandler";
const fields = [
{ type: "text", name: "description", placeholder: "Exercise Description" },
{ type: "number", name: "duration", placeholder: "Duration (in minutes)" },
{
type: "date",
name: "date",
placeholder: "Date"
}
];
// utilize the custom useFormHandler hook within a functional component and
// pass it an object with some initial state.
const CreateExerciseForm = () => {
const { values, handleChange, handleSubmit } = useFormHandler({
username: "",
description: "",
duration: "",
date: "",
error: ""
});
// the below will show a spinner if "values.users" hasn't been fulfilled yet
// else, it'll show the form fields. in addition, if there's ever a
// "values.error", it'll be displayed to the user.
return (
<form
style={{ width: 500, margin: "0 auto", textAlign: "center" }}
onSubmit={handleSubmit}
>
{isEmpty(values.users) ? (
<Spinner name="line-scale" />
) : (
<Fragment>
<Select
name="username"
placeholder="Select a user..."
handleChange={handleChange}
value={values.username}
selectOptions={values.users}
style={{ width: "100%" }}
/>
{fields.map(({ name, type, placeholder }) => (
<Input
key={name}
type={type}
name={name}
placeholder={placeholder}
onChange={handleChange}
value={values[name]}
/>
))}
<Button type="submit">
<FaCalendarPlus style={{ position: "relative", top: 2 }} />
Create Exercise
</Button>
</Fragment>
)}
{values.error && <p>{values.error}</p>}
</form>
);
};
export default CreateExerciseForm;
State as independent data types
Or, if you insist on using separated states, then create a resetValue function in the useFormInput hook:
const useFormInput = initialValue => {
// initialize state from "initialValue"
const [value, setValue] = useState(initialValue)
// handle changes to the "value" state via updating it
// with e.target.value
const handleChange = useCallback(({ target: { value } => {
setValue(value)
}, []);
// reset the value back to initialValue
const resetValue = useCallback(() => {
setValue(initialValue);
}, []);
return {
value,
handleChange,
resetValue
}
}
Then, destructure properties for the username (and other states, if needed):
const CreateExercise = () => {
// use ES6 destructure and aliasing to extract and rename the
// "value" (as username), "handleChange" function (as
// handleUsernameChange) and "resetValue" function (as resetUsername)
const {
value: username,
handleChange: handleUsernameChange,
resetValue: resetUsername
} = useFormInput('')
...other form state
...useEffect(() => {}, [])
const handleSubmit = useCallback(e => {
e.preventDefault();
const exercise = {
username: username,
description: description,
duration: duration,
date: date
};
axios
.post('http://localhost:5000/exercises/add', exercise)
.then(res => {
console.log(res.data)
// only reset the username if the exercise was successfully
// created
resetUsername();
})
.catch(err => console.log(err.toString());
}, [date, description, duration, resetUsername, username]);
return ( ...form )
}
I took a look and did a PR - Formik implementation w/validation.
Here is the PR - https://github.com/jeltehomminga/mern-tracker/pull/1
UI View
<>
<h3>Create New Exercise Log</h3>
<pre>{JSON.stringify({ formData }, null, 2)}</pre>
<ExerciseForm {...{ users }} onChange={data => setFormData(data)} />
</>
CreateExercise Form
import React from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import DatePicker from "react-datepicker";
import cx from "classnames";
const requiredMessage = "Required";
const exerciseFormSchema = Yup.object().shape({
username: Yup.string().required(requiredMessage),
description: Yup.string()
.min(2, "Too Short!")
.required(requiredMessage),
duration: Yup.number()
.integer()
.min(1, "Min minutes!")
.max(60, "Max minutes!")
.required(requiredMessage),
date: Yup.string().required(requiredMessage)
});
const ExerciseForm = ({ users = [], onChange }) => {
return (
<Formik
initialValues={{
username: "",
description: "",
duration: "",
date: ""
}}
validationSchema={exerciseFormSchema}
onSubmit={values => onChange(values)}
>
{({
values,
touched,
errors,
handleChange,
handleBlur,
isSubmitting,
setFieldValue
}) => {
const getProps = name => ({
name,
value: values[name],
onChange: handleChange,
onBlur: handleBlur,
className: cx("form-control", {
"is-invalid": errors[name]
})
});
return isSubmitting ? (
// Replace this with whatever you want...
<p>Thanks for the Exercise!</p>
) : (
<Form>
<FormControl label="Username">
<>
<select {...getProps("username")}>
<>
<option value="default">Select user...</option>
{users.map(person => (
<option key={person} value={person.toLowerCase()}>
{person}
</option>
))}
</>
</select>
<FormErrorMessage {...{ errors }} name="username" />
</>
</FormControl>
<FormControl label="Description">
<>
<Field {...getProps("description")} />
<FormErrorMessage {...{ errors }} name="description" />
</>
</FormControl>
<FormControl label="Duration in minutes">
<>
<Field {...getProps("duration")} type="number" />
<FormErrorMessage {...{ errors }} name="duration" />
</>
</FormControl>
<FormControl label="Date">
<>
{/* Was present before refactor */}
<div>
<DatePicker
{...getProps("date")}
selected={values.date}
minDate={new Date()}
onChange={date => setFieldValue("date", date)}
/>
<FormErrorMessage {...{ errors }} name="date" />
</div>
</>
</FormControl>
<button type="submit" className="btn btn-primary">
Create Exercise log
</button>
</Form>
);
}}
</Formik>
);
};
export default ExerciseForm;
// Created to manage label and parent className
const FormControl = ({ label, children }) => (
<div className="form-group">
<label>{label}:</label>
{children}
</div>
);
const FormErrorMessage = ({ name, errors }) => {
const error = errors && errors[name];
return error ? (
<div
class="invalid-feedback"
// Add inline style override as error message cannot sit as sibling to datePicker (bootstrap css)
style={{ display: "block" }}
>
{error}
</div>
) : null;
};