Skip to content

Instantly share code, notes, and snippets.

@WebDeg-Brian
Last active November 12, 2019 13:36
Show Gist options
  • Save WebDeg-Brian/feae9fa83ca199c39008d08254c5dd07 to your computer and use it in GitHub Desktop.
Save WebDeg-Brian/feae9fa83ca199c39008d08254c5dd07 to your computer and use it in GitHub Desktop.
React validation hook for reusablitily's sake
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&#39;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));
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