I have a simple React app that I am going to post entirely (it is not that long). The app doesn't work but doesn't throw any error either. I tried to log the states and it turns out they never change. I am using big things like custom hook and useReducer, but I suspect I lack on understanding basic principles of how react works.
Here's a short summary of how the app should work:
There is a Form
component which returns a series of custom Input
elements (here only two).
The Input
component outsources the validation logic to a custom hook which returns [isTouched, isValid, dispatcherOfTheCustomHookReducer]
. When an event occurs, the Input
component calls the dispatcher of the custom hook and then styles should be applied to the <input>
element based on the state returned by the reducer in the custom hook.
Also since the Form component needs to know if the form as a whole is valid, each Input has an onChangeValidity
property used to lift up the isValid
state. In theory the form should appear neutral at the beginning and then, after you focus and blur an input this should become either valid (blue background) or invalid (red background).
I should probably reset the inputs after submission and add something else, but for now I want to make the app work. At the moment the states never changes and the forms appears always neutral (white).
You may prefer look at the files in codesandbox .
App.js
import Form from './components/Form';
function App() {
return (
<div className="app">
<Form />
</div>
);
}
export default App;
Form.js
import { useReducer } from 'react';
import Input from './Input';
// put your inputs' ID here to generate the default state
const defaultState = (inputs = ['username', 'email']) => {
let inputsState = {};
for (const input of inputs) inputsState[input] = false;
return { ...inputsState, isValid: false };
};
const formReducer = (state, action) => {
let newInputsStateList = {...state, [action.id]: action.isValid};
delete newInputsStateList.isValid;
let isValid = true;
for(const key in newInputsStateList) {
if(!newInputsStateList[key]) isValid = false;
break;
}
return { ...newInputsStateList, isValid};
}
const Form = props => {
const [formState, dispatchFormState] = useReducer(formReducer, undefined, defaultState);
const submitHandler = event => {
event.preventDefault();
console.log('You are logged in.');
}
return <form onSubmit={submitHandler}>
<Input
id='username'
label='Username'
type='text'
test={username => username.trim().length > 6}
onChangeValidity={validity => dispatchFormState({id: 'username', isValid: validity})}
/>
<Input
id='email'
label='Email'
type='email'
test={email => email.includes('@')}
onChangeValidity={validity => dispatchFormState({id: 'email', isValid: validity})}
/>
<button type='submit' disabled={!formState.isValid} >Submit</button>
</form>
};
export default Form;
Input.js
import { useEffect } from 'react';
import classes from './Input.module.css';
import useValidation from '../hooks/use-validation';
const Input = props => {
const [isTouched, isValid, checkValidity] = useValidation();
// eslint-disable-next-line
useEffect(() => props.onChangeValidity(isValid), [isValid]);
return <div className={classes.generic_input}>
<label className={classes['generic_input-label']} htmlFor={props.id} >{props.label}</label>
<input
className={classes[`${isTouched ? 'generic_input-input--'+isValid ? 'valid' : 'invalid' : ''}`]}
type={props.type}
name={props.id}
id={props.id}
onChange={event => checkValidity({
type: 'CHANGE',
value: event.target.value,
test: props.test
})}
onBlur={event => checkValidity({
type: 'BLUR',
value: event.target.value,
test: props.test
})}
/>
</div>
};
export default Input;
use-validation.js
import { useReducer } from 'react';
const validationReducer = (state, action) => {
let isTouched = state.isTouched;
let isValid = state.isValid;
if(action.type === 'CHANGE') if (isTouched) isValid = action.test(action.value);
else if(action.type === 'BLUR') {
isValid = action.test(action.value);
if (!isTouched) isTouched = true;
}
else isTouched = isValid = false;
return {isTouched, isValid};
}
const useValidation = () => {
const [validationState, dispatchValidation] = useReducer(validationReducer, {isTouched: false, isValid: false});
return [validationState.isTouched, validationState.isValid, dispatchValidation];
};
export default useValidation;
Input.module.css
.generic_input {
display: flex;
flex-direction: column;
padding: 1rem;
}
.generic_input-label {
font-weight: bold;
}
.generic_input-input--valid {
background-color: lightblue;
}
.generic_input-input--invalid {
border-color: red;
background-color: rgb(250, 195, 187);
}
.submit:disabled {
background-color: #CCC;
color: #292929;
border-color: #CCC;
cursor: not-allowed;
}
I think you need to fix the isTouched
logic in your validationReducer
. isTouched
never gets set to true
:
Something like:
const validationReducer = (state, action) => {
let isTouched = state.isTouched;
let isValid = state.isValid;
if (action.type === "CHANGE") {
isTouched = true;
isValid = action.test(action.value)
} else if (action.type === "BLUR") {
isValid = action.test(action.value);
} else {
isTouched = isValid = false;
}
return { isTouched, isValid };
};
... though I'm not sure when you'd want isTouched
to be set to false again, so that logic needs some work...
Also, the class on your input is not correct.
Its should look like:
<input
className={
classes[
isTouched
? `generic_input-input--${isValid ? "valid" : "invalid"}`
: ""
]
}
...
>
Take a look at this sandbox
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.