I've got a problem with sending emails in React by EmailJs. When i validate form and all the errors desapears, form is sending email only after second click and i dont really know why this is happening why. Please help
const useForm = (callback, validate) => {
const [values, setValues] = useState({
title: "",
email: "",
message: "",
});
const [errors, setErrors] = useState({});
const [send, setSend] = useState(false);
const [isSubmiting, setIsSubmiting] = useState(false);
useEffect(() => {
if (Object.keys(errors).length === 0) {
if (isSubmiting) {
setSend(true);
}
}
}, [errors]);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value,
});
};
const handleSubmit = (e) => {
e.preventDefault();
setErrors(validate(values));
setIsSubmiting(true);
if (send) {
emailjs
.sendForm(
"service",
"templatekey",
e.target,
"userkey"
)
.then(
(result) => {
console.log(result.text);
},
(error) => {
console.log(error.text);
}
);
e.target.reset();
}
};
return { handleChange, values, handleSubmit, errors };
};
export default useForm;
After moving setErrors(validate(values)) and setIsSubmiting(true) to handleChange it works fine for me :)
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value,
});
setErrors(validate(values));
setIsSubmiting(true);
};
const handleSubmit = (e) => {
e.preventDefault();
if (send) {
console.log("WYSYĆAM");
emailjs
.sendForm(
"service",
"template",
e.target,
"user"
)
.then(
(result) => {
console.log(result.text);
},
(error) => {
console.log(error.text);
}
);
e.target.reset();
}
};
return { handleChange, values, handleSubmit, errors };
};
I ran into the same problem and found that adding an 'onClick' event handler to your form button will prevent the user from needing to double click.
<button onClick={handleClick}> Submit </button>
in useForm.js I moved setErrors(validate(values)); and setIsSubmitting(true); into handleClick
function handleClick() {
setErrors(validate(values));
setIsSubmitting(true);
};
const handleSubmit = e => {
e.preventDefault();
if (send) {
emailjs.sendForm('service', 'templateKey', e.target, 'userKey')
.then((result) => {
console.log(result.text);
}, (error) => {
console.log(error.text);
});
callback();
}
};
Related
I'm a little new to react and i can't understand why my object property is undefined but when i console.log my object is appearing okay see this screenshot:
This is my custom hook useForm:
const useForm = (callback, validateRegister) => {
const [values, setValues] = useState({
name: '',
email: '',
password: '',
confirmPass: '',
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setValues({
...values,
[name]: value,
});
};
const handleSubmit = (event) => {
event.preventDefault();
setErrors(validateRegister(values)); // validateReister is another function that returns and object with these properties.
setIsSubmitting(true);
};
useEffect(() => {
if (Object.keys(errors).length === 0 && isSubmitting) {
callback();
}
}, [errors]);
return {
handleChange,
handleSubmit,
values,
errors,
};
};
export default useForm;
Component:
const { handleChange, handleSubmit, values, errors } = useForm(
submit,
validateRegister
);
Problem:
{errors.nameError}
Is not showing up, is not appearing on console.log either. Any idea?
I think your validateRegister(values) returns a Promise. Try changing your implementation to the below :-
const handleSubmit = (event) => {
event.preventDefault();
validateRegister(values).then(data => setErrors(data)).catch(err => console.log(err));
setIsSubmitting(true);
};
Replace setErrors(validateRegister(values)); with
validateRegister(values).then(data => setErrors(data)).catch(e => console.log(e));
My code
UserEdit.jsx
import useForm from "../../utils/useForm";
import LoadingBtn from "../../utils/loadingButton";
import { getUser } from "../../store/users/userActions";
const UserEdit = () => {
//declare form data and scmena
const formInput = {
name: "",
};
const schema = {
name: Joi.string().required().min(3).max(191).label("Name"),
};
//dispatch on first mount
const dispatch = useDispatch();
const params = useParams();
useEffect(() => {
setErrors({});
dispatch(getUser(params.id));
}, []);
const {
handleChange,
handleSubmit,
formData,
setFormData,
errors,
setErrors,
} = useForm(formInput, doSubmit, schema);
function doSubmit() {
console.log("handle Submit", formData);
}
const isSubmitting = useSelector((state) => state.users.isSubmitting);
const user = useSelector((state) => state.users.user);
useEffect(() => {
console.log("useEffect user >>", user);
setFormData({
...formData,
name: user.name,
});
}, [user]);
return (
<input
className={`form-control ${
errors["name"]
? "is-invalid"
: ""
}`}
type="text"
id="name"
name="name"
required=""
placeholder="Enter your name"
onChange={handleChange}
value={formData.name}
/>
);
};
export default UserEdit;
UseForm.jsx
const useForm = (formInput, callback, schema = {}) => {
const [formData, setFormData] = useState(formInput);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setFormData({
...formData,
[name]: value,
});
};
const handleSubmit = (e) => {
e.preventDefault();
//handle error
setErrors(validate(formData));
setIsSubmitting(true);
};
const validate = (formData) => {
const { error } = Joi.validate(formData, schema, {
abortEarly: false,
});
if (!error) return {};
const validataionErrors = {};
for (let item of error.details) {
validataionErrors[item.path[0]] = item.message;
}
return validataionErrors;
};
useEffect(() => {
//check if there are any errors
if (Object.keys(errors).length === 0 && isSubmitting) {
callback();
setIsSubmitting(true);
}
}, [errors]);
return {
handleChange,
handleSubmit,
formData,
setFormData,
errors,
setErrors,
};
};
export default useForm;
I googled about the error and it mentioned that state need to be initialized at first with the field. However, I have already defined initial state as
const formInput = {
name: "",
};
I could not find how could I fix this, I am open to restructure the useForm hooks if that is the one which causing trouble.
currently, if I uncomment the following line on userEdit.jsx, the warning will be gone, but also the edit form becomes empty as well
setFormData({
...formData,
name: user.name,
});
I have created this custom hook to fetch data:
const useSuggestionsApi = () => {
const [data, setData] = useState({ suggestions: [] });
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
const fetchData = () => {
setError(false);
setLoading(true);
if(url) {
fetch(url).then((res) => {
if (res.status !== 200) {
console.error(`It seems there was an problem fetching the result. Status Code: ${res.status}`)
return;
}
res.json().then((fetchedData) => {
setData(fetchedData)
})
}).catch(() => {
setError(true)
})
setLoading(false);
};
}
fetchData();
}, [url]);
return [{ data, loading, error }, setUrl];
}
export default useSuggestionsApi;
It used used in this component to render the response (suggestions).
const SearchSuggestions = ({ query, setQuery}) => {
const [{ data }, doFetch] = useSuggestionsApi();
const { suggestions } = data;
useEffect(() => {
const encodedURI = encodeURI(`http://localhost:3000/search?q=${query}`);
doFetch(encodedURI);
}, [doFetch, query]);
return (
<div className="search-suggestions__container">
<ul className="search-suggestions__list">
{suggestions.map((suggestion) => {
return (
<li className="search-suggestions__list-item" key={uuid()}>
<span>
{suggestion.searchterm}
</span>
</li>
)
})}
</ul>
</div>
);
};
export default SearchSuggestions;
Now I would like to write some unit test for the SearchSuggestions component but I am lost on how to mock the returned data from useSuggestionApi. I tried importing useSuggestionApi as a module and then mocking the response like this but with no success:
describe('SearchSuggestions', () => {
const wrapper = shallow(<SearchSuggestions/>)
it('test if correct amount of list-item elements are rendered', () => {
jest.mock("../hooks/useSuggestionsApi", () => ({
useSuggestionsApi: () => mockResponse
}));
expect(wrapper.find('.search-suggestions__list').children()).toHaveLength(mockResponse.data.suggestions.length);
});
})
I am new to testing React components so very grateful for any input!
This works:
jest.mock('../hooks/useSuggestionsApi', () => {
return jest.fn(() => [{data: mockResponse}, jest.fn()]
)
})
describe('SearchSuggestions', () => {
const wrapper = shallow(<SearchSuggestions query="jas"/>)
it('correct amount of list-items gets rendered according to fetched data', () => {
expect(wrapper.find('.search-suggestions__list').children()).toHaveLength(mockResponse.suggestions.length);
});
})
I am working on displaying a "message" on the component based on the server response, and i wanted that message to disappear after 5 second. I tried my best with setTimeout but no luck, can you help me?
Here is my code:
import React, { useState } from "react";
import { Form, Button, Container, Row, Col} from 'react-bootstrap'
import axios from 'axios'
export default function Users() {
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [message, setMessage] = useState("")
function handleSubmit(e){
e.preventDefault()
const credential = { email, name };
axios
.post('/', credential)
.then(response => {
if(response.status === 201) {
resetInputs()
setMessage(response.data.message)
}
})
.catch(error => {
if (error.response.status === 409) {
setMessage(error.response.data.message)
}
})
}
function resetInputs(){
setEmail("")
setName("")
}
return (
<div className="form">
<div className="hero-container">
<h1>Welcome to <span className="hi">my</span><span>website</span></h1>
<h5>Enter your name and your email to join our waiting list!</h5>
<p></p>
<div>
{message}
</div>
<p></p>
</div>
)
}
You call setTimeout after setting the message, telling it to fire after five seconds, and then clear the message:
function handleSubmit(e){
e.preventDefault()
const credential = { email, name };
axios
.post('/', credential)
.then(response => {
if(response.status === 201) {
resetInputs()
setMessage(response.data.message)
}
})
.catch(error => {
if (error.response.status === 409) {
setMessage(error.response.data.message)
}
})
.finally(() => { // ***
setTimeout(() => { // ***
setMessage(""); // *** If you want to clear the error message as well
}, 5000); // *** as the normal message
}); // ***
}
or
function handleSubmit(e){
e.preventDefault()
const credential = { email, name };
axios
.post('/', credential)
.then(response => {
if(response.status === 201) {
resetInputs()
setMessage(response.data.message)
setTimeout(() => { // *** If you only want to automatically clear
setMessage(""); // *** this message and not an error message
}, 5000); // ***
}
})
.catch(error => {
if (error.response.status === 409) {
setMessage(error.response.data.message)
}
});
}
You can add setTimout to your axios call, or you can reset it independently like this:
import { useEffect } from "react";
...
useEffect(() => {
let isUnmounted = false;
if (message !== "") {
setTimeout(() => {
if (!isUnmounted ) {
setMessage("");
}
}, 5000);
}
return () => { isUnmounted = true; };
}, [message])
isUnmounted prevents using setMessage() in an unmounted component, it is possible for a user to close the component before time is reached.
Something like this may work (untested):
const useTimedState = (initialState, duration) => {
const [state, setState] = setState(initialState);
useEffect(() => {
if (typeof state === 'undefined') {
return;
}
const timer = setTimeout(() => {
setState();
}, duration);
return () => clearTimeout(timer);
}, [state]);
return [state, setState];
}
export default function Users() {
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [message, setMessage] = useTimedState(undefined, 5000);
function handleSubmit(e){
e.preventDefault()
const credential = { email, name };
axios
.post('/', credential)
.then(response => {
if(response.status === 201) {
resetInputs()
setMessage(response.data.message)
}
})
.catch(error => {
if (error.response.status === 409) {
setMessage(error.response.data.message)
}
})
}
}
I'm trying to log users in based on a isLoggedin state from redux store. My Login Component dispatches the fetchLogin action from the userLogin action mapped to props.
const Login = (props) => {
const [userLogin, setUserLogin] = useState({ email: "", password: "" })
const getValue = e => {
e.preventDefault();
setUserLogin({
...userLogin,
[e.target.name]: e.target.value
});
}
const makeRequest = (e) => {
e.preventDefault()
props.userLogin(userLogin)
if (props.isLoggedin === true) {
console.log(Cookie.get('accessToken'))
props.history.push('/dashboard')
}
}
return (
<Fragment>
<Form onSubmit={makeRequest}>
<Form.Group controlId="formBasicEmail">
<Form.Label>Email Address</Form.Label>
<Form.Control
type="email"
name="email"
placeholder="Enter email"
onChange={getValue}
>
</Form.Control>
</Form.Group>
<Form.Group controlId="formBasicPassword">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
name="password"
placeholder="Enter password"
onChange={getValue}
>
</Form.Control>
</Form.Group>
<Button
variant="primary"
type="submit"
>
Login
</Button>
</Form>
</Fragment>
)
}
const mapStateToProps = (state) => ({
isFetching: state.userReducer.isFetching,
isLoggedin: state.userReducer.isLoggedin,
isError: state.userReducer.isError,
errMessage: state.userReducer.errMessage
});
const mapDispatchToProps = dispatch => ({
userLogin: (userLogin) => {
dispatch(fetchLogin(userLogin))
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Login)
The problem here is when the user clicks login, the redux action I have which takes care of logging the user in from the backend takes time and the store isn't updated fast enough for the userLogin function to confirm that the user is logged in. I understand this has something to do with promises but I'm not sure how to do it exactly.
You can find my actions below, the only action that matters is the fetchLogin which is the one being dispatched from the login component.
export const userLoginRequest = () => {
return {
type: USER_LOGIN_REQUEST,
payload: {
isFetching: true
}
};
}
export const userLoginSuccess = (user) => {
return {
type: USER_LOGIN_SUCCESS,
payload: {
user: {
...user
},
isLoggedin: true,
isFetching: false
}
};
}
export const userLoginFailed = () => {
return {
type: USER_LOGIN_FAILED,
payload: {
isFetching: false,
isError: true
}
};
}
export const fetchLogin = (userLogin) => dispatch => {
dispatch(userLoginRequest())
axios.post('/login', {
email: userLogin.email,
password: userLogin.password
})
.then(response => {
if (response.status === 200) {
dispatch(userLoginSuccess(response.data))
}
}, err => {
console.log("Error", err)
dispatch(userLoginFailed())
})
}
You can return the promise from your action and set isLoggedin state after it's resolved:
export const fetchLogin = (userLogin) => dispatch => {
dispatch(userLoginRequest())
return axios.post('/login', {
email: userLogin.email,
password: userLogin.password
})
.then(response => {
if (response.status === 200) {
dispatch(userLoginSuccess(response.data))
}
return response;
}).catch(err => {
console.log("Error", err)
dispatch(userLoginFailed())
})
}
// ...
const makeRequest = (e) => {
e.preventDefault()
props.userLogin(userLogin).then(resp => {
if (props.isLoggedin === true) {
console.log(Cookie.get('accessToken'))
props.history.push('/dashboard')
}
})
}
Also for this to work you need to update mapDispatchToProps to actually return the dispatch:
const mapDispatchToProps = dispatch => ({
userLogin: (userLogin) => dispatch(fetchLogin(userLogin))
});
One more thing, I just realised that to properly redirect in this case using the promise from dispatch is actually not necessary since it's possible to track the isLoggedin prop in useEffect
const makeRequest = (e) => {
e.preventDefault()
props.userLogin(userLogin)
}
useEffect(() => {
if (props.isLoggedin === true) {
console.log(Cookie.get('accessToken'))
props.history.push('/dashboard')
}
}, [props.isLoggedin])