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>
);
};
I have this edit page where the initial value of the first name, last name, and address should be from the data retrieved from the firebase firestore using the useEffect.
useEffect(() => {
const unsubscribe = firestore
.collection("users")
.doc(uid)
.onSnapshot((snapshot) => {
const arr = [];
arr.push({
...snapshot.data(),
});
setUsers(arr);
});
return () => {
unsubscribe();
};
}, []);
handleSubmit to save the updated information in the firestore:
const handleSubmit = async (e) => {
e.preventDefault();
try {
const userRef = firestore.collection("users").doc(uid);
const ref = userRef.set(
{
firstName,
middleName,
lastName,
address,
},
{ merge: true }
);
console.log(" saved");
} catch (err) {
console.log(err);
}
};
Below is the entire code:
const edit = () => {
const uid = location.state;
const [isLoading, setIsLoading] = useState(false);
const [users, setUsers] = useState([]);
const [firstName, setFirstName] = useState("");
useEffect(() => {
const unsubscribe = firestore
.collection("users")
.doc(uid)
.onSnapshot((snapshot) => {
const arr = [];
arr.push({
...snapshot.data(),
});
setUsers(arr);
setIsLoading(true);
});
return () => {
unsubscribe();
};
}, []);
const handleSubmit = (e) => {
e.preventDefault();
console.log(firstName);
};
return (
<div>
{isLoading ? (
<>
{users &&
users.map((user) => (
<li style={{ listStyle: "none" }}>
<CardHeader title="Update Profile" />
<form onSubmit={handleSubmit}>
<TextField
type="text"
value={user.firstName}
variant="outlined"
label="First Name"
fullWidth
onChange={(e) => setFirstName(e.target.value)}
/>
<Button type="submit"> Submit</Button>
</form>
</li>
))}
</>
) : (
<h1>Loading...</h1>
)}
</div>
);
};
export default edit;
Let's say I wanted to edit the address of user "John Park". Those text fields should have the initial value of the first and last names of John so even if the address field was the only one that was updated, the fields first name and last name won't be an empty string when saved in the firestore. How should I do this? Thank you.
It seems you are updating the local state, but it isn't initialized to anything but an empty string.
Your updates should be to the local copy of users you fetched, and the submit handler should grab a specific user to update in your backend.
const [users, setUsers] = React.useState([]);
Change handler - curried function to take index and return handler. It maps the previous users state to the next, updating the user at the specified index.
const changeHandler = index => e => {
const { name, value } = e.target;
setUsers(users => users.map((user, i) => i === index
? {
...user,
[name]: value,
}
: user,
));
};
Submit handler - curried function to take index of specific user object to send to backend.
const handleSubmit = index => async (e) => {
e.preventDefault();
try {
const userRef = firestore.collection("users").doc(uid);
const ref = userRef.set(
{ ...users[index] }, // <-- user by index
{ merge: true }
);
console.log(" saved");
} catch (err) {
console.log(err);
}
};
Pass the current mapped index to the handlers. Add the name attribute to each field that matches the user property name for the input.
return (
<div>
{isLoading ? (
<>
{users &&
users.map((user, index) => (
<li style={{ listStyle: "none" }}>
<CardHeader title="Update Profile" />
<form onSubmit={handleSubmit(index)}> // <-- pass index
<TextField
type="text"
value={user.firstName}
variant="outlined"
label="First Name"
name="firstName" // <-- add name attribute
fullWidth
onChange={changeHandler(index)} // <-- pass index to handler
/>
... other fields
<Button type="submit">Submit</Button>
</form>
</li>
))}
</>
) : (
<h1>Loading...</h1>
)}
</div>
);
I'm new to Redux work, trying to learn by doing. Here I have AntD input, when user writes something then it saves it to the object keys billingName: and billingContactPerson, but I have also two buttons sender and receiver, when user clicks sender button then it takes data from redux and put it to input, but my question is how to save that data to the same billingName and billingContactPerson. I have tried to save it in useEffect billingName = PickUpName, but it did not save it.
My code:
let billingName: any;
let billingContactPerson: any;
const userData = useSelector(selectUserData);
const dispatch = useDispatch();
const DeliveryName = userData.deliveryName;
const PickUpName = userData.pickUpName;
const DeliveryContactPerson = userData.deliveryContactPerson;
const PickUpContactPerson = userData.pickUpContactPerson;
const [name, setName] = useState(billingName);
const [contactPerson, setContactPerson] = useState(
billingContactPerson
);
const [payer, setPayer] = useState("");
useEffect(() => {
const names = () => {
if (payer === "receiver") {
billingName = DeliveryName;
dispatch(changeUserData({ ...userData, billingName }));
}
if (payer === "sender") {
billingName = PickUpName;
dispatch(changeUserData({ ...userData, billingName }));
} else {
return billingName;
}
};
setName(names);
const contactPersons = () => {
if (payer === "receiver") {
billingContactPerson = DeliveryContactPerson;
dispatch(changeUserData({ ...userData, billingContactPerson }));
}
if (payer === "sender") {
billingContactPerson = PickUpContactPerson;
dispatch(changeUserData({ ...userData, billingContactPerson }));
} else {
return billingContactPerson;
}
};
setContactPerson(contactPersons);
}, [payer]);
const senderPays = (e: any) => {
e.preventDefault();
setPayer("sender");
};
const receiverPays = (e: any) => {
e.preventDefault();
setPayer("receiver");
};
<div>
<Button onClick={senderPays}>sender</Button>
<Button onClick={receiverPays}>receiver</Button>
<Form.Item
label={t("o.billingName")}
name="billingName"
initialValue={userData["billingName"] || name || ""}
>
<Input
onChange={(e: any) =>
dispatch(
changeUserData({ ...userData, billingName: e.target.value })
)
}
type="string"
/>
</Form.Item>
<Form.Item
label={t("orders.ContactPerson")}
name="billingContactPerson"
initialValue={
userData["billingContactPerson"] ||
contactPerson ||
""
}
>
<Input
onChange={(e: any) =>
dispatch(
changeUserData({
...userData,
billingContactPerson: e.target.value,
})
)
}
type="string"
/>
</Form.Item>
</div>
If you're new I recommend you to start with reduxjs/toolkit. It is the new recommended way of writting redux logic.
Let's Learn modern redux
About the question you asked. You can try triggering an function after the redux logic or after sending the data to the input field and give type to billingName and billingContactPerson. So, that you can more catch errors.
I have a user requirement when adding a form it should check if the name of the form is already exist. How can I do that in es6? I'm using AntDesign and ReactJS.
Here's my code
<Form.Item label="Form Name">
{getFieldDecorator('formName', {
rules: [formConfig],
})(<Input name="formName" onChange={onChange} />)}
</Form.Item>
const handleChange = e => {
const { name, value } = e.target;
setState(() => ({
...state,
[name]: value,
}));
let isExist = [...formDataSource];
let foundExistItem = isExist.filter(
item => item.formName === formName
);
};
If you want dynamically query the form fields, you should wrap your form with Form.create.
It injects useful functionalities like onFieldsChange listener:
const onFieldsChange = (_, changedFiels) => {
const { username } = changedFiels;
if (username) {
// Make your API checks
console.log(`Now changing ${username.name}`);
}
};
const ValidatedFields = Form.create({ onFieldsChange })(App);
Note: You should keep your Form.Items uncontrolled using getFieldDecorator therefore avoiding onChange while collecting form data.
If you still insist to make form items controlled, you should use getFieldValue:
const handleChange = e => {
const { name, value } = e.target;
const { getFieldValue } = form;
setState(() => ({
...state,
[name]: value
}));
// or use getFieldValue for a single query
const values = getFieldsValue(['formName',...more fields]);
if (values.length) {
// API CALL
}
};
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;
};