首页 > 解决方案 > 在 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;

标签: reactjshigher-order-components

解决方案


每个渲染似乎都导致原始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() {
  ...

编辑 React 输入 HOC - 已修复


推荐阅读