reactjs - 在 React 中使用 HOC 创建受控输入
问题描述
我正在尝试创建一个呈现受控输入的 HOC,但我所有的努力都导致输入在 onChange 之后失去焦点。我认为这是密钥的基本问题,但摆弄密钥似乎没有帮助,我花了几个小时进入各种“带输入的 HOC”博客/示例,似乎找不到任何来自父节点的受控输入状态。我究竟做错了什么?提前致谢!
编辑:HOC 的目标是返回一个验证钩子。添加其他代码后,我为该问题创建了一个沙箱,希望能有所帮助。
type WrapperProps = {
name: string;
label: string;
onChange?: (event: any) => void;
state: any;
}
export const InputWrapper = (v: ValidationObject) => {
const Wrapper: FC<WrapperProps> = (props) => {
const { label, onChange, name, state } = props;
const getPattern = (value: any) => {
return v.getFieldValid(name)
? `${value}`
: `${randomString()}`
};
const modifiedProps = {
name,
onBlur: () => v.validate(name, prop(name, state), state),
onChange,
pattern: getPattern(state[name]),
value: state[name],
};
return (
<React.Fragment>
<label htmlFor={name}>{label}</label>
<input key={name} id={name} {...modifiedProps} />
<p style={{ color: 'red' }}>{v.getError(name)}</p>
</React.Fragment>
);
}
return Wrapper;
}
我的验证钩子将导出 HOC 的参数:
验证钩子.ts
import { useState } from 'react';
import { prop, map, all, indexOf, mergeDeepRight } from 'ramda';
import { createValidationsState, compose, isEqual } from 'util/utilities';
import { InputWrapper } from './wrapper2';
export interface ValidationArray<T> {
key: keyof T;
value: unknown;
}
export interface ErrorMessages {
[key: string]: string;
}
export interface ValidationFunction {
(val: any, state: any): boolean | string | number;
}
// Dictionary of Booleans
export interface ValidationState {
[key: string]: {
isValid: boolean;
error: string;
};
}
// Dictionary of validation definitions
export interface ValidationProps {
errorMessage: string;
validation: ValidationFunction;
}
export interface ValidationSchema {
[key: string]: ValidationProps[];
}
export interface ValidationObject {
getError: Function;
getFieldValid: Function;
isValid: boolean;
validate: Function;
validateAll: Function;
validateIfTrue: Function;
validationState: ValidationState;
}
/**
* A hook that can be used to generate an object containing functions and
* properties pertaining to the validation state provided.
* @param validationSchema an object containing all the properties you want to validate
* @returns object { getError, getFieldValid, isValid, validate, validateAll, validateIfTrue, validationState }
*/
export const useValidation = <S>(validationSchema: ValidationSchema) => {
const [isValid, setIsValid] = useState<boolean>(true);
const [validationState, setValidationState] = useState<ValidationState>(
createValidationsState(validationSchema)
);
/**
* Executes the value against all provided validation functions and
* updates the state.
* @param key string the name of the property being validated
* @param value any the value to be tested for validation
* @return true/false validation
*/
const runAllValidators = (key: string, value: any, state?: S) => {
const runValidator = compose(
(func: Function) => func(value, state),
prop('validation')
);
const bools: boolean[] = map(runValidator, validationSchema[key]);
const isValid: boolean = all(isEqual(true), bools);
const index: number = indexOf(false, bools);
const error = index > -1 ? validationSchema[key][index].errorMessage : '';
const validations: any = {};
validations[key] = { isValid, error };
return validations;
}
/**
* executes a validation function on a value and updates isValid state
* @param key string the name of the property being validated
* @param value any the value to be tested for validation
* @return true/false validation
*/
const validate = (key: string, value: any, state?: S) => {
if (key in validationSchema) {
const validations = runAllValidators(key, value, state);
setValidationState(mergeDeepRight(validationState, validations));
setIsValid(validations[key].isValid);
return validations[key].isValid;
}
};
/**
* updates isValid state if validation succeeds
* @param key string the name of the property being validated
* @param value any the value to be tested for validation
* @return void
*/
const validateIfTrue = (key: string, value: unknown, state?: S) => {
if (key in validationSchema) {
const validations = runAllValidators(key, value, state);
if (validations[key].isValid) {
setValidationState(mergeDeepRight(validationState, validations));
}
}
};
/**
* Runs all validations against an object with all values and updates/returns
* isValid state.
* @param state any an object that contains all values to be validated
* @return boolean isValid state
*/
const validateAll = (state: S) => {
const bools = map((key: string) => {
return validate(key, state[key as keyof S], state);
}, Object.keys(validationSchema));
const result = all(isEqual(true), bools);
setIsValid(result);
return result;
};
/**
* Get the current error stored for a property on the validation object.
* @param key the name of the property to retrieve
* @return string
*/
const getError = (key: string) => {
if (key in validationSchema) {
const val = compose(
prop('error'),
prop(key),
);
return val(validationState);
}
return '';
};
/**
* Get the current valid state stored for a property on the validation object.
* @param key the name of the property to retrieve
* @return boolean
*/
const getFieldValid = (key: string) => {
if (key in validationSchema) {
const val = compose(
prop('isValid'),
prop(key),
);
return val(validationState);
}
return true;
};
const validationObject = {
getError,
getFieldValid,
isValid,
validate,
validateAll,
validateIfTrue,
validationState,
}
// inititally where I wanted to use the HOC and make it one
// of the available exports
const ValidationWrap = InputWrapper(validationObject);
return {
...validationObject,
ValidationWrap
};
};
BasicInput.validation.ts
import {useValidation} from 'validation.hook';
export interface Dog {
name: string;
breed: string;
}
export const BasicInputValidation = () => {
return useValidation<Dog>({
name: [
{
errorMessage: 'Cannot be Bob.',
validation: (val: string, state: any) => {
return val.trim().toLowerCase() !== 'bob';
}
},
{
errorMessage: 'Cannot be Ross.',
validation: (val: string, state: any) => {
return val.trim().toLowerCase() !== 'ross';
}
},
{
errorMessage: 'Name is required.',
validation: (val: string, state: any) => {
return val.trim().length > 0;
}
},
],
breed: [
{
errorMessage: 'Must be a Leonberger.',
validation: (val: string, state: any) => {
return val.trim().toLowerCase() === 'leonberger';
}
},
{
errorMessage: 'Breed is required.',
validation: (val: string, state: any) => {
return val.trim().length > 0;
}
},
]
});
};
实用程序.ts
(createValidationState 是运行代码所必需的,其他是为了方便复制意大利面)
/**
* Creates a random 7 character string.
* @return string
*/
export const randomString = () => Math.random().toString(36).substring(7);
/**
* Compose function that is a little more friendly to use with typescript.
* @param fns any number of comma-separated functions
* @return new function
*/
export const compose = (...fns: Function[]) => (x: any) =>
fns.reduceRight((y: any, f: any) => f(y), x);
// Build Validation State Object
export const createValidationsState = (schema: ValidationSchema) => {
const keys = Object.keys(schema);
return keys.reduce(
(prev: any, item: string) => {
prev[item] = {
isValid: true,
error: ''
};
return prev;
},
{}
);
};
下面是一个使用示例:
import React, { useState } from 'react';
import {BasicInputValidation} from 'examples/basicInput.validation';
import {InputWrapper} from 'withValidationComponent';
import { curry } from 'ramda';
function App() {
const [state, setState] = useState<{name: string}>({ name: '' });
const onChange = curry((name: string, event: any) => {
const data = { [name]: event.target.value }
setState({ ...state, ...data });
})
const v = BasicInputValidation();
const HOC = InputWrapper(v);
return (
<>
<HOC
name="name"
label="Name"
onChange={onChange('name')}
state={state}
/>
</>
);
}
export default App;
解决方案
每个渲染似乎都导致原始InputWrapper
组件卸载。此时正是由于这段代码const HOC = InputWrapper({});
——每次在父节点上发生渲染时,都会生成一个新的包装器。这在我的实验中很明显:
export const InputWrapper = (v) => {
const Wrapper = (props) => {
const { label, onChange, name, state } = props;
useEffect(() => {
return () => {
console.log("unmounting"); // cleanup got invoked everytime I typed in the input
};
}, []);
该错误的 CodeSandBox:https ://codesandbox.io/s/react-input-hoc-bugged-e28iz?file=/src/withValidationComponent.js
为了解决这个问题,在实现方面(即在 App 组件上),我将包装器实例移到了函数之外
import React, { useState } from "react";
import {BasicInputValidation} from 'examples/basicInput.validation';
import {InputWrapper} from 'withValidationComponent';
import { curry } from 'ramda';
const HOC = InputWrapper({}); // <-- moved this here
function App() {
...
推荐阅读
- javascript - NodeORM 按日期月份分组
- wordpress - 如何修复 Godaddy 中的 508 资源限制。进入过程显示最大超过
- clojure - 有条件地绑定来自 Clojure 核心的动态 var 以实现向后兼容性
- excel - 如果出现错误,则出现“下一步”问题“错误”结束 VBA
- angular - Angular 6验证数字输入
- azure - 无法使用 NotificationHub + ApnsCredentials 部署 arm 模板
- scala - 错误:值 toDF 不是 org.apache.spark.rdd.RDD[org.apache.kafka.clients.consumer.ConsumerRecord[String,String]] 的成员
- c# - 如何从winforms中的Outlook电子邮件中获取附件?
- clojure - 我可以强制 lein 忽略当前 Clojure 项目外部任何代码的依赖关系吗?
- android-studio - Android Studio 代理设置