I need to check the fields and i need to pass several properties to the button. String, Object, Array, Func and i need usestate be an object like example. Any solution to not cause re-rendering?
EDIT: I need to do a way that when the user fills in the input the button does not re-render.
EDIT 2: when I use the primitive type useState(string). And I make the comparison works , useState of type Object making comparison causes re-rendering on button:
code below does not cause re-rendering
const [email, setEmail] = useState("");
const validateFields = useCallback(() => {
if (email === "") {
alert("Empty e-mail.");
return false;
}
return true;
}, [email]);
code below does CAUSE re-rendering - (comparison with object)
const [userData, setUserData] = useState({
email: "",
password: ""
});
const validateFields = useCallback(() => {
if (userData.email === "") {
alert("Empty e-mail.");
return false;
}
return true;
}, [userData]);
project link : https://codesandbox.io/s/tender-allen-tfklv8?file=/src/components/Button.js
import { useCallback, useState } from "react";
import Button from "./components/Button";
import "./styles.css";
export default function App() {
const [userData, setUserData] = useState({
email: "",
password: ""
});
const validateFields = useCallback(() => {
if (userData.email.trim() === "") {
alert("Empty e-mail.");
return false;
}
return true;
}, [userData]);
const handleAPI = useCallback(() => {
if (validateFields()) {
console.log("handleAPI");
}
}, [validateFields]);
return (
<div className="App">
<input
value={userData.email}
onChange={(event) =>
setUserData((prev) => ({
...prev,
email: event.target.value
}))
}
/>
<Button onClick={handleAPI} />
</div>
);
}
const Button = ({ onClick }) => {
console.log("------------Button render---------------");
return <button onClick={onClick}>BUTTON DEFAULT</button>;
};
export default memo(Button);
If you're intending to use the validateData function when clicking on the button, there's no need for the handleAPI function to be within a useCallback hook that depends on validateData. Whenever you click on the button, the validateData function will be called and will validate the desired data with whatever value is currently in the userData state variable.
Related
I am creating a project where we can add and edit a user. For editing a user I am sending id as a url parameter using react-router-dom Link from one page to other and with the useParams hook. I am fetching that id. This id I am using for setting the data to form. I have all users stored in redux store. Now the question is when I reload the page the data is taking some time to load from redux store and the setValue of useFormHook is throwing an error.
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import { fetchData } from "./utils/index";
import { UpdateStudent} from "./utils/index";
function Update() {
const users = useSelector((state) => state.students);
console.log(users);
const [students, setStudents] = useState([]);
const {
register,
handleSubmit,
setValue,
formState: { errors, isDirty, isValid },
} = useForm({ mode: "onChange" });
let { id } = useParams();
var user = students && students.filter((u) => u.id === parseInt(id));
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
setStudents(users);
}, [users]);
useEffect(() => {
setValue("name", user.name);
}, []);
const onUpdate = (data) => {
};
return (
<form onSubmit={handleSubmit(onUpdate)}>
<div>
<input
{...register("name", {
})}
type="text"
className="form-control"
placeholder="Name"
/>
</div>
<button
color="primary"
type="submit"
disabled={!isDirty || !isValid}
>
Save
</button>
</form>
);
}
export default Update;
The way I solve this in my current project is that I split the component into 2, one component gets the user, one is only rendered when the user is available.
function Update() {
const users = useSelector((state) => state.students);
console.log(users);
const [students, setStudents] = useState([]);
let { id } = useParams();
var user = students && students.filter((u) => u.id === parseInt(id));
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
setStudents(users);
}, [users]);
if(!user) return null; // or progress indicator
return <UpdateForm user={user} />
}
function UpdateForm({user})
const {
register,
handleSubmit,
setValue,
formState: { errors, isDirty, isValid },
} = useForm({ mode: "onChange" });
useEffect(() => {
setValue("name", user.name);
}, []);
const onUpdate = (data) => {
};
}, [user.name]);
return (
<form onSubmit={handleSubmit(onUpdate)}>
<div>
<input
{...register("name", {
})}
type="text"
className="form-control"
placeholder="Name"
/>
</div>
<button
color="primary"
type="submit"
disabled={!isDirty || !isValid}
>
Save
</button>
</form>
);
}
export default Update;
Besides the issues I mentioned in comment above, the code doesn't handle when the students state updates. You could simplify the code a bit and consume the students state directly instead of duplicating it locally. Since the redux state is sometimes not available on the initial render cycle you'll need to use an useEffect with a dependency on the state used to update form state.
function Update() {
const users = useSelector((state) => state.students);
const {
register,
handleSubmit,
setValue,
formState: { errors, isDirty, isValid },
} = useForm({ mode: "onChange" });
const { id } = useParams();
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
const user = users?.filter((u) => u.id === Number(id));
if (user) {
setValue("name", user.name);
}
}, [users]); // <-- add users to dependency to update form state
...
if (/* no form state yet */) {
return "Loading..."; // or some loading indicator
}
return (
... // form UI
);
}
I am trying to update the database. So I have an input field that is disabled as default. So when you click, editing is enabled and when you click outside of the input field, it gets disabled again. What I am trying to do is update when you click outside of the input field. So, my input is like this:
const InputPrice = ({ mainPricePosts, handleChange }) => {
const [disabled, setDisabled] = useState(true);
const [priceValue, setPriceValue] = useState(mainPricePosts);
function handleClick() {
if (disabled === true) {
setDisabled(false);
}
}
return (
<>
<Form.Control
type="text"
className="price_coefficient_input"
value={priceValue}
onBlur={() => {
setDisabled(true);
handleChange(priceValue);
}}
onChange={handleChange(mainPricePosts)}
readOnly={disabled}
onClick={handleClick}
/>
</>
);
};
InputPrice.propTypes = {
mainPricePosts: PropTypes.object.isRequired,
handleChange: PropTypes.func.isRequired,
};
export default InputPrice;
And this is how I am trying to update but I am not sure if I am doing right to get the value from the input field:
const [updatePosts, setUpdatePosts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [show, setShow] = useState(false);
const [showError, setShowError] = useState(false);
const handleClose = () => setShow(false);
const handleCloseError = () => setShowError(false);
const fetchIndividualPosts = async ({ value, post: { mainPricePosts, key } = {} }) => {
console.log(value);
try {
setLoading(true);
const res = await Axios({
method: "POST",
url: `url`,
headers: {
"content-Type": "application/json",
},
data: {
updated_parameter: ["main_price", "small_car", key],
updated_value: value,
},
});
if (res.status === 200) {
setUpdatePosts(res.data);
}
setLoading(false);
} catch (err) {
console.log(err.response.status);
setError(err.response.data.error);
setLoading(false);
}
};
const handleChange = (mainPricePosts) => (e) => {
fetchIndividualPosts({ mainPricePosts, value: e.target.value });
};
This is also the curl how I can update the data:
curl -L -i -H "Content-Type: application/json" -X POST -d '{
"updated_parameter":["100"],
"updated_value":"0.044"
}' $ip''
so updated_value should be the updated input (the value after, outside is clicked)
100, should be the key of the input value.
Hope it is clear and you can help me about this problem.
Thanks for your help beforehand.
There are many ways you can achieve what you need, but I would use following approach.
In your InputPrice component on onBlur event I would disable input by calling setDisabled(true) and then use useEffect hook to call handleChange callback if new price value and original price values are different. Because you are calling setDisabled(true), you're actually re-rendering your InputPrice component and therefore not executing handleChange callback.
Checkout code below.
const InputPrice = ({ mainPricePosts, handleChange }) => {
const [disabled, setDisabled] = useState(true);
const [priceValue, setPriceValue] = useState(mainPricePosts);
function handleClick() {
if (disabled === true) {
setDisabled(false);
}
}
useEffect(() => {
let callUpdateCallback = false;
if (priceValue !== mainPricePosts) callUpdateCallback = true;
if (disabled && callUpdateCallback) handleChange(priceValue);
}, [disabled, priceValue, handleChange, mainPricePosts]);
return (
<>
<Form.Control
type="text"
className="price_coefficient_input"
value={priceValue}
onBlur={setDisabled(true)}
onChange={(e) => setPriceValue(e.target.value)}
readOnly={disabled}
onClick={handleClick}
/>
</>
);
};
InputPrice.propTypes = {
mainPricePosts: PropTypes.object.isRequired,
handleChange: PropTypes.func.isRequired,
};
export default InputPrice;
You call this component like this
import React from "react";
import ReactDOM from "react-dom";
import InputPrice from "./InputPrice";
function App() {
const handleChange = (e) => {
console.log("app handle change", e);
// You can call your fetch here...
};
return (
<div>
<InputPrice mainPricePosts="500" handleChange={handleChange} />
</div>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
Additionally there codesandbox that used to debug it, so if you need more details you can find it on the link below.
https://codesandbox.io/s/reactjs-playground-forked-8vwe2?file=/src/index.js:0-364
I have a form inside a route, that if there are any validation errors, it should not allow the user to navigate to another route. If there are no validation errors, then allow navigation to another route.
Below is my current code, which the onBlock function does not work does to its async nature as the functions to submit and then validate the form are asynchronous.
FormComponent.js
import React, { useState, useEffect, useRef } from "react";
import { FieldArray } from "formik";
import { useHistory } from "react-router-dom";
import * as Yup from "yup";
import Form from "./Form";
import TextInput from "./TextInput";
const FormComponent = () => {
const history = useHistory();
const [initialValues, setInitialValues] = useState();
const [isSubmitted, setIsSubmitted] = useState(false);
const block = useRef();
const formRef = useRef(null);
const onFormSubmit = async (values) => {
setIsSubmitted(true);
};
const validationSchema = () => {
const schema = {
test: Yup.string().required("Input is Required")
};
return Yup.object(schema);
};
const onBlock = () => {
const { submitForm, validateForm } = formRef?.current || {};
// submit form first
submitForm()
.then(() => {
// then validate form
validateForm()
.then(() => {
// if form is valid - should navigate to route
// if form is not valid - should remain on current route
return formRef?.current.isValid;
})
.catch(() => false);
})
.catch(() => false);
};
const redirectToPage = () => {
history.push("/other-page");
};
useEffect(() => {
block.current = history.block(onBlock);
return () => {
block.current && block.current();
};
});
useEffect(() => {
if (isSubmitted) redirectToPage();
}, [isSubmitted]);
useEffect(() => {
setInitialValues({
test: ""
});
}, []);
return initialValues ? (
<Form
initialValues={initialValues}
onSubmit={onFormSubmit}
formRef={formRef}
validationSchema={validationSchema}
>
<FieldArray
name="formDetails"
render={(arrayHelpers) =>
arrayHelpers && arrayHelpers.form && arrayHelpers.form.values
? (() => {
const { form } = arrayHelpers;
formRef.current = form;
return (
<>
<TextInput name="test" />
<button type="submit">Submit</button>
</>
);
})()
: null
}
/>
</Form>
) : null;
};
export default FormComponent;
If a user tries to submit the form without any value in the input, I would expect that onBlock would return false to block navigation. But this does not seem to work. Simply returning false in the onBlock function does however. So it seems that the history.block function does not accept any callbacks. I have also tried to convert it to an async function and await the submitForm & validateForm functions, but still no joy. Is there a way around this? Any help would be greatly appreciated.
Here is CodeSandbox with an example.
The history.block function accepts a prompt callback which you can use to prompt the user or do something else in response to the page being blocked. To block the page you just need to call history.block() more info here.
The formik form is validated when you try to submit it and if it successfully validates then it proceeds to submit the form, this is when onSubmit callback will be called. So if you'd like to block the page when there are validation errors you can use the formik context to subscribe to the validation isValid and whenever that is false block.
const useIsValidBlockedPage = () => {
const history = useHistory();
const { isValid } = useFormikContext();
useEffect(() => {
const unblock = history.block(({ pathname }) => {
// if is valid we can allow the navigation
if (isValid) {
// we can now unblock
unblock();
// proceed with the blocked navigation
history.push(pathname);
}
// prevent navigation
return false;
});
// just in case theres an unmount we can unblock if it exists
return unblock;
}, [isValid, history]);
};
Here is a codesandbox for that adapted from yours. I removed some components that weren't needed.
Another solution is validating manually on all page transitions and choosing when to allow the transition yourself and in this case it is if validateForm returns no errors.
// blocks page transitions if the form is not valid
const useFormBlockedPage = () => {
const history = useHistory();
const { validateForm } = useFormikContext();
useEffect(() => {
const unblock = history.block(({ pathname }) => {
// check if the form is valid
validateForm().then((errors) => {
// if there are no errors this form is valid
if (Object.keys(errors).length === 0) {
// Unblock the navigation.
unblock();
// retry the pagination
history.push(pathname);
}
});
// prevent navigation
return false;
});
return unblock;
}, [history, validateForm]);
};
And the codesandbox for that here
I am trying to make an update user page with previous information to be rendered inside the input fields. Console.log returns the correct value but its not showing up as the initial value inside of the useState.
Getting previous user bio
function EditProfile(props) {
const user = useSelector(state => state.user);
const [profile, setProfile] = useState([])
const userId = props.match.params.userId
const userVariable = {
userId: userId
}
useEffect(() => {
axios.post('/api/users/getProfile', userVariable)
.then(response => {
if (response.data.success) {
console.log(response.data)
setProfile(response.data.user)
} else {
alert('Failed to get user info')
}
})
}, [])
console.log(profile.bio);
Heres what I am currently using to display the input field. (edited for brevity)
const [bio, setBio] = useState("");
const handleChangeBio = (event) => {
console.log(event.currentTarget.value);
setBio(event.currentTarget.value);
}
return (
<label>Bio</label>
<TextArea
id="bio"
onChange={handleChangeBio}
value={bio}
/>
)
Was trying to do this before but object was not showing up as the useState initial value
const [bio, setBio] = useState(User.bio);
Back-end - I know that $set overrides all information, so was trying to render the previous information inside of the input fields so it would not be overrided with blank values.
router.post('/edit', auth, (req, res)=> {
console.log(req.body.education)
User.updateMany(
{ _id: req.user._id },
[ {$set: { bio: req.body.bio}},
{$set: { industry: req.body.industry}},
{$set: { jobTitle: req.body.jobTitle}},
],
(err)=>{
if (err) return res.json({success: false, err});
return res.status(200).send({
success: true
});
});
});
Create some custom component and put User as props and you will see that you get data.
const [User, setUser] = useState([])
better to change to
const [user, setUser] = useState('')
You can get some issues because components starts with capital letter
And array as default value may error after first render
You can move it to separate component:
<Example user={user} />
const Example = (props) => {
const [bio, setBio] = useState(props.user.bio);
const handleChangeBio = (event) => {
console.log(event.currentTarget.value);
setBio(event.currentTarget.value);
}
return (
<label>Bio</label>
<TextArea
id="bio"
onChange={handleChangeBio}
value={bio}
/>
)
}
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 />;
};