Reputation: 73
I have a login form made from a map of a FormInput component.
I have now created separate FormComponents depending on the question type as defined by a prop.
However, since making this change my 'credentials' state is not updating on a user keystroke.
I would like the CREDENTIALS state to be updated on keystroke. However, this is not happening currently.
LoginForm
import React from 'react';
import FormInput from '../../components/Form Input/FormInput';
import { loginInputs } from '../../formSource/formSourceData';
import './Login.scss';
import { useState, useContext } from 'react';
import { useNavigate, Link, useLocation } from 'react-router-dom';
import { useToastContext } from '../../context/toastContext';
import useFetch from '../../Hooks/UseFetch';
import { AuthContext } from '../../context/AuthContext'
import axios from 'axios'
const Login = () => {
const [credentials, setCredentials] = useState({
email: undefined,
password: undefined,
});
const { user, loading, error, dispatch} = useContext(AuthContext)
const { toastDispatch } = useToastContext();
const navigate = useNavigate();
const { state } = useLocation()
const handleChange = (e) => {
console.log(e.target.name)
setCredentials({ ...credentials, [e.target.name]: e.target.value });
console.log(credentials)
};
const handleSubmit = async (e) => {
e.preventDefault()
dispatch({type: "LOGIN_START"})
try{
const res = await axios.post("/api/auth/login", credentials)
dispatch({type:"LOGIN_SUCCESS", payload: res.data})
navigate(state?.path || '/auth/teacher/dashboard');
} catch(err) {
dispatch({type: "LOGIN_FAILURE", payload: err.response.data})
}
}
return (
<div className="container">
<div className="formWrapper">
<h1>Login</h1>
<form className="loginForm" >
{loginInputs.map((input) => (
<FormInput
key={input.id}
{...input}
handleChange={handleChange}
/>
))}
<button type="submit" className="loginButton" onClick={handleSubmit}>
Login
</button>
</form>
<p className="forgotPassword">
<Link to="/forgot-password">Forgot Password</Link>
</p>
<p className="accountText">
Not signed up? <Link to="/register">Register</Link>
</p>
</div>
</div>
);
};
export default Login;
FormInputComponent
import { InputRounded } from '@mui/icons-material';
import React, { useState } from 'react';
import './formInput.scss';
const FormInput = (props) => {
const { label, type, errorMessage, handleChange, id, value, ...inputProps } =
props;
const [focused, setFocused] = useState(false);
const [passwordShown, setPasswordShown] = useState(false);
const handleFocus = (e) => {
setFocused(true);
};
const togglePassword = () => {
setPasswordShown(!passwordShown);
};
const Dropdown = () => {
return (
<select
className="formElementInput"
value={value}
name={inputProps.name}
onChange={handleChange}
>
<option className="default" selected disabled>
{inputProps.placeholder}
</option>
{inputProps.options.map((option) => (
<option className="option" value={option}>
{option}
</option>
))}
</select>
);
};
const Input = () => {
return (
<div className="formGroup">
<input
className="formElementInput"
value={value}
name={props.name}
placeholder={props.placeholder}
type={passwordShown ? 'text' : type}
onChange={props.handleChange}
onBlur={handleFocus}
focused={focused.toString()}
onFocus={() =>
inputProps.name === 'confirmPassword' && setFocused(true)
}
/>
<span className="icon" onClick={togglePassword}>
{passwordShown ? inputProps.icon : inputProps.opposite}
</span>
</div>
);
};
return (
<div className="formElement">
<label className="formElementLabel">{label}</label>
{type === 'dropdown' ? (
<Dropdown />
) : (
<Input/>
)}
<span className="errorMessage">{errorMessage}</span>
</div>
);
};
export default FormInput;
LOGIN INPUT CODE
export const loginInputs = [
{
id: 1,
name: "email",
type: "email",
placeholder: "Email",
label: "Email",
errorMessage: "Enter a valid email address",
required: true
},
{
id: 2,
name: "password",
type: "password",
placeholder: "Password",
label: "Password",
errorMessage: "A password should be more than 8 characters.",
required: true,
icon: <Visibility/>,
opposite: <VisibilityOff/>
}
]
This is the original working code prior to separating.
import { InputRounded } from '@mui/icons-material';
import React, { useState } from 'react';
import './formInput.scss';
const FormInput = (props) => {
const { label, type, errorMessage, handleChange, id, value, ...inputProps } =
props;
const [focused, setFocused] = useState(false);
const [passwordShown, setPasswordShown] = useState(false);
const handleFocus = (e) => {
setFocused(true);
};
const togglePassword = () => {
setPasswordShown(!passwordShown);
};
const Dropdown = () => {
return (
<select
className="formElementInput"
value={value}
name={inputProps.name}
onChange={handleChange}
>
<option className="default" selected disabled>
{inputProps.placeholder}
</option>
{inputProps.options.map((option) => (
<option className="option" value={option}>
{option}
</option>
))}
</select>
);
};
const Input = () => {
return (
<div className="formGroup">
<input
className="formElementInput"
value={value}
name={props.name}
placeholder={props.placeholder}
type={passwordShown ? 'text' : type}
onChange={props.handleChange}
onBlur={handleFocus}
focused={focused.toString()}
onFocus={() =>
inputProps.name === 'confirmPassword' && setFocused(true)
}
/>
<span className="icon" onClick={togglePassword}>
{passwordShown ? inputProps.icon : inputProps.opposite}
</span>
</div>
);
};
return (
<div className="formElement">
<label className="formElementLabel">{label}</label>
{type === 'dropdown' ? (
<select
className="formElementInput"
value={value}
name={inputProps.name}
onChange={handleChange}
>
<option className="default" selected disabled>
{inputProps.placeholder}
</option>
{inputProps.options.map((option) => (
<option className="option" value={option} >
{option}
</option>
))}
</select>
) : (
<div className="formGroup">
<input
className="formElementInput"
value={value}
name={inputProps.name}
placeholder={inputProps.placeholder}
type={passwordShown ? "text" : type}
onChange={handleChange}
onBlur={handleFocus}
focused={focused.toString()}
onFocus={() =>
inputProps.name === 'confirmPassword' && setFocused(true)
}
/>
<span className="icon" onClick={togglePassword}>
{passwordShown ? inputProps.icon : inputProps.opposite}
</span>
</div>
)}
<span className="errorMessage">{errorMessage}</span>
</div>
);
};
export default FormInput;
Here is a link to the code sandbox which exhibits the same behaviour I have explained above.
https://codesandbox.io/s/thirsty-feather-9fwwwy?file=/src/App.js
Upvotes: 0
Views: 69
Reputation: 4572
The reason is that your <Input />
component inside FormComponent.jsx
is controlled.. so it looks for the value
attribute in rendered input field, but could not find any:
return (
<div className="formGroup">
<input
className="formElementInput"
name={props.name}
value={props.value}
placeholder={props.placeholder}
type={passwordShown ? "text" : props.type}
onChange={props.handleChange}
onBlur={handleFocus}
focused={focused.toString()}
onFocus={() => props.name === "confirmPassword" && setFocused(true)}
/>
// ...
Because.. there is not any:
// Notice no value `key` on any of these objects:
export const loginInputs = [
{
id: 1,
name: "email",
type: "email",
placeholder: "Email",
label: "Email",
errorMessage: "Enter a valid email address",
required: true
},
{
id: 2,
name: "password",
type: "password",
placeholder: "Password",
label: "Password",
errorMessage: "A password should be more than 8 characters.",
required: true,
icon: "Icon",
opposite: "Icon2"
}
];
to fix that, you can provide a value attribute, depending upon your credentials
state:
{loginInputs.map((input) => (
<FormInput
key={input.id}
value={credentials[input.name] ?? ""} // provide value prop
{...input}
handleChange={handleChange}
/>
))}
Bonus: I noticed that the input lost it's focus every time you typed there(maybe this was not the problem in your code, just in Sandbox you provided).
This was because you declared <Input />
inside the <FormInput />
so everytime it changed, it basically created that <Input />
component from scratch. These are many ways to do it, but the easiest is to just use it inline in ternary:
{props.type === "dropdown" ? (
<Dropdown /> // Also do the same for `Dropdown` if face the similar isssue
) : (
// used inline
<div className="formGroup">
<input
className="formElementInput"
name={props.name}
value={props.value}
placeholder={props.placeholder}
type={passwordShown ? "text" : props.type}
onChange={props.handleChange}
onBlur={handleFocus}
focused={focused.toString()}
onFocus={() => props.name === "confirmPassword" && setFocused(true)}
/>
<span className="icon" onClick={togglePassword}>
{passwordShown ? props.icon : props.opposite}
</span>
</div>
)}
Here's the link to updated Sandbox:
https://codesandbox.io/s/epic-poitras-84z4do?file=/Login.jsx:943-995
Upvotes: 1