@@ -61,6 +61,9 @@ export default function useForm(inputConfigs = []) {
6161 // track whether or not input has been focused
6262 const [hasBeenFocused, setHasBeenFocused] = useSetState({});
6363
64+ // track whether or not the input has been touched
65+ const [hasBeenTouched, setHasBeenTouched] = useSetState({});
66+
6467 // build fn that transforms a value by input name
6568 const transform = useCallback(
6669 (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
@@ -97,8 +100,11 @@ export default function useForm(inputConfigs = []) {
97100
98101 // on change, transform and set value
99102 const onChange = useCallback(
100− name => e => setValue(name, getValue(name, e)), //
101− [setValue, getValue]
103+ name => e => {
104+ setValue(name, getValue(name, e));
105+ setHasBeenTouched({ [name]: true });
106+ }, //
107+ [setValue, getValue, setHasBeenTouched]
102108 );
103109
104110 // on focus, update focus
@@ -126,27 +132,45 @@ export default function useForm(inputConfigs = []) {
126132 const datas = useMemo(() => validations.map(v => v.data), [validations]);
127133
128134 // the input has errored if it
135+ // 1) did not pass validation and has an error text
136+ // 2) or has a specific error from parent state
137+ // and then we store that in errorTexts[i]
138+ // <string | falsy>[]
139+ const errorTexts = useMemo(
140+ () =>
141+ configs.map((config, i) => validations[i].error || config.error || false),
142+ [configs, validations]
143+ );
144+
145+ // we should hint at an error if it
129146 // 1) has been focused before
130− // 2)
131− // a) did not pass validation and has an error text
132− // b) or has a specific error from parent state
133− // (and we implicitly return the error text at the end)
134− const errors = useMemo(
147+ // 2) and has an error
148+ // boolean[]
149+ const hintErrors = useMemo(
135150 () =>
136151 configs.map(
137− (config, i) =>
138− hasBeenFocused[config.name] && //
139− (validations[i].error || config.error)
152+ (config, i) => hasBeenFocused[config.name] && !!errorTexts[i]
140153 ),
141− [configs, validations, hasBeenFocused]
154+ [configs, errorTexts, hasBeenFocused]
155+ );
156+
157+ // we should block on error (with potential reflow) if it
158+ // 1) has been touched
159+ // 2) and has an error
160+ // consumers should use this value to show error text and block on operations
161+ // <string | falsy>[]
162+ const errors = useMemo(
163+ () =>
164+ configs.map((config, i) => hasBeenTouched[config.name] && errorTexts[i]),
165+ [configs, errorTexts, hasBeenTouched]
142166 );
143167
144168 // this input has passed if
145169 // 1) it has passed local validation
146170 // 2) there are no errors
147171 const passes = useMemo(
148− () => validations.map((v, i) => v.pass && !errors[i]),
149− [validations, errors]
172+ () => validations.map((v, i) => v.pass && !errorTexts[i]),
173+ [validations, errorTexts]
150174 );
151175
152176 // visibly tell the user that their input has passed if
@@ -176,6 +200,7 @@ export default function useForm(inputConfigs = []) {
176200 pass: passes[i],
177201 visiblyPassed: visiblePasses[i],
178202 error: errors[i],
203+ hintError: hintErrors[i],
179204 focused: focuses[i],
180205 autoFocus: autoFocus && !disabled,
181206 disabled,
@@ -198,6 +223,7 @@ export default function useForm(inputConfigs = []) {
198223 visiblePasses,
199224 passes,
200225 errors,
226+ hintErrors,
201227 focuses,
202228 values,
203229 onBlur,