Last active
November 12, 2019 13:36
-
-
Save WebDeg-Brian/feae9fa83ca199c39008d08254c5dd07 to your computer and use it in GitHub Desktop.
React validation hook for reusablitily's sake
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useCallback, useState, useEffect } from 'react'; | |
import PropTypes from 'prop-types'; | |
import { TextField, Button, Link as MuiLink } from '@material-ui/core'; | |
import { withRouter, Link } from 'react-router-dom'; | |
import { isEmpty, isEmail } from 'validator'; | |
import { connect } from 'react-redux'; | |
import { useFormField } from 'app-hooks'; | |
import firebase from 'app-firebase'; | |
import { FormBox, ButtonWrapper } from '../shared'; | |
const mapStateToProps = state => ({ | |
authUser: state.authUser, | |
}); | |
const LoginBox = ({ history, authUser }) => { | |
const [ | |
{ email, password }, | |
handleFieldChange, | |
handleFieldBlur, | |
handleFieldFocus, | |
validateFieldAfterSubmit, | |
] = useFormField({ | |
email: { | |
value: '', | |
validationRules: [ | |
{ | |
method: isEmpty, | |
validWhen: false, | |
errorText: 'This field should not be left empty!', | |
}, | |
{ | |
method: isEmail, | |
errorText: 'This field should contain an email!', | |
}, | |
], | |
}, | |
password: { | |
value: '', | |
validationRules: [ | |
{ | |
method: isEmpty, | |
validWhen: false, | |
errorText: 'This field should not be left empty!', | |
}, | |
], | |
}, | |
}); | |
const [errorText, setErrorText] = useState(''); | |
const [isLoading, setIsLoading] = useState(false); | |
const handleFormSubmit = useCallback( | |
async e => { | |
e.preventDefault(); | |
setErrorText(''); | |
if (!validateFieldAfterSubmit('email', email.value)) return; | |
if (!validateFieldAfterSubmit('password', password.value)) return; | |
setIsLoading(true); | |
try { | |
await firebase.auth().signInWithEmailAndPassword(email.value, password.value); | |
} catch (err) { | |
setIsLoading(false); | |
setErrorText(err.message); | |
} | |
}, | |
[validateFieldAfterSubmit, email.value, password.value], | |
); | |
useEffect(() => { | |
if (authUser) history.push('/dashboard'); | |
}, [authUser, history]); | |
return ( | |
<FormBox | |
title="Login" | |
formLink={ | |
<MuiLink component={Link} to="/register"> | |
Haven't got an account yet? Register! | |
</MuiLink> | |
} | |
isLoading={isLoading} | |
onSubmit={handleFormSubmit} | |
errorText={errorText} | |
> | |
<TextField | |
label="Email" | |
margin="dense" | |
variant="outlined" | |
name="email" | |
value={email.value} | |
onChange={handleFieldChange} | |
onBlur={handleFieldBlur} | |
onFocus={handleFieldFocus} | |
fullWidth | |
helperText={email.errorText} | |
error={!email.isValid} | |
/> | |
<TextField | |
label="Password" | |
margin="dense" | |
variant="outlined" | |
name="password" | |
type="password" | |
value={password.value} | |
onChange={handleFieldChange} | |
onBlur={handleFieldBlur} | |
onFocus={handleFieldFocus} | |
fullWidth | |
helperText={password.errorText} | |
error={!password.isValid} | |
/> | |
<ButtonWrapper dense> | |
<Button variant="contained" type="submit" color="primary"> | |
Submit | |
</Button> | |
</ButtonWrapper> | |
</FormBox> | |
); | |
}; | |
LoginBox.propTypes = { | |
/** | |
* The authenticated user | |
*/ | |
authUser: PropTypes.object, | |
/** | |
* @ignore | |
*/ | |
history: PropTypes.object.isRequired, | |
}; | |
export default withRouter(connect(mapStateToProps)(LoginBox)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useCallback, useEffect, useRef } from 'react'; | |
import debounce from 'lodash.debounce'; | |
import partition from 'lodash.partition'; | |
export default fields => { | |
const initialFieldState = {}; | |
for (const key of Object.keys(fields)) | |
initialFieldState[key] = { | |
value: fields[key].value, | |
isValid: true, | |
errorText: null, | |
}; | |
const [fieldState, setFieldState] = useState(initialFieldState); | |
// Capture the state in advance as functional components capture state at render | |
// https://overreacted.io/how-are-function-components-different-from-classes/ | |
const latestFieldState = useRef(initialFieldState); | |
const validate = useCallback((valueToValidate, rules) => { | |
for (let i = 0; i < rules.length; i += 1) { | |
const { method, params = [], validWhen = true, errorText } = rules[i]; | |
if (method(valueToValidate, ...params) !== validWhen) return [false, errorText]; | |
} | |
return [true, null]; | |
}, []); | |
const enhanceValidationRules = useCallback(rules => { | |
const [crossFieldRules, otherRules] = partition(rules, ({ compareTo }) => !!compareTo); | |
const enhancedCrossFieldRules = crossFieldRules.map(rule => ({ | |
...rule, | |
params: [latestFieldState.current[rule.compareTo].value], | |
})); | |
return [...otherRules, ...enhancedCrossFieldRules]; | |
}, []); | |
const validateField = useCallback( | |
debounce((name, value, callback) => { | |
const currentRules = fields[name].validationRules; | |
const enhancedRules = enhanceValidationRules(currentRules); | |
const [isValid, errorText] = validate(value, enhancedRules); | |
setFieldState(prevState => ({ | |
...prevState, | |
[name]: { | |
...prevState[name], | |
isValid, | |
errorText, | |
}, | |
})); | |
if (callback != null) callback(isValid); | |
}, 2000), | |
[], | |
); | |
const handleFieldChange = useCallback( | |
e => { | |
const { name, value } = e.target; | |
setFieldState(prevState => ({ | |
...prevState, | |
[name]: { | |
...prevState[name], | |
value, | |
}, | |
})); | |
validateField(name, value); | |
}, | |
[validateField], | |
); | |
const handleFieldBlur = useCallback( | |
e => { | |
const { name, value } = e.target; | |
validateField(name, value); | |
// Should validate immediately when user unfocuses the field | |
validateField.flush(); | |
}, | |
[validateField], | |
); | |
const handleFieldFocus = useCallback( | |
e => { | |
const { name, value } = e.target; | |
setFieldState(prevState => ({ | |
...prevState, | |
[name]: { | |
...prevState[name], | |
errorText: null, | |
}, | |
})); | |
// Validate after the user focuses on the field | |
validateField(name, value); | |
}, | |
[validateField], | |
); | |
const validateFieldAfterSubmit = useCallback( | |
(name, value) => { | |
let pass; | |
validateField(name, value, isValid => { | |
pass = isValid; | |
}); | |
validateField.flush(); | |
return pass; | |
}, | |
[validateField], | |
); | |
// Clean up debounce in case it sets the state after unmounting | |
useEffect(() => () => validateField.cancel(), [validateField]); | |
// Update the ref | |
useEffect(() => { | |
latestFieldState.current = fieldState; | |
}, [fieldState]); | |
return [ | |
fieldState, | |
handleFieldChange, | |
handleFieldBlur, | |
handleFieldFocus, | |
validateFieldAfterSubmit, | |
]; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment