Search code examples
reactjsreact-forwardrefreact-custom-hooks

forwardRef with custom component and custom hook


Edit: Small changes for readability.

I'm new to react and I may be in at the deep end here but I'll go ahead anyway..

I have a Login component in which I want to give the users feedback when the input elements lose focus and/or when the user clicks submit.

I am aware that I achieve a similar bahavior with useState but for the sake of education I'm trying with useRef.

I'm getting a TypeError for undefined reading of inputRef in LoginForm.js. So inputRef is not assigned a value when validateInput is called. Can anyone help me make sense of why that is and whether there is a solution to it?

LoginForm.js:

import useInput from '../../hooks/use-input';
import Input from '../../UI/Input/Input';

 const LoginForm = () => {
  const { inputRef, isValid } = useInput(value =>
    value.includes('@')
  );

  return <Input ref={inputRef} />;
};

use-input.js (custom hook):

const useInput = validateInput => {
  const inputRef = useRef();
  const isValid = validateInput(inputRef.current.value);
  return {
    inputRef,
    isValid,
  };
};

Input.js (custom element component):

const Input = forwardRef((props, ref) => {
  return <input ref={ref} {...props.input}></input>;
});

Solution

  • One issue that I'm seeing is that in the Input component, you're using props.input, why?

    const Input = forwardRef((props, ref) => {
      return <input ref={ref} {...props}></input>;
    });
    

    You want exactly the props that you're sending to be assigned to the component.

    Next up, you're doing value.includes('@'), but are you sure that value is not undefined?

    const { inputRef, isValid } = useInput(value =>
        value && value.includes('@')
      );
    

    This would eliminate the possibility of that error.


    Solving the issue with the inputRef is undefined is not hard to fix.

    Afterward, you're going to face another issue. The fact that you're using useRef (uncontrolled) will not cause a rerender, such that, if you update the input content, the isValid won't update its value.

    Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. (React Docs)

    This is a personal note, but I find uncontrolled components in general hard to maintain/scale/..., and also refs are not usually meant to do this kind of stuff. (yes, yes you have react-form-hook which provides a way of creating forms with uncontrolled components, and yes, it's performant).

    In the meantime, while I'm looking into this a little more, I can provide you a solution using useState.

    const useInput = (validationRule, initialValue='') => {
      const [value, setValue] = useState(initialValue)
    
      const onChange = (e) => setValue(e.target.value)
    
      const isValid = validationRule && validationRule(value)
    
      return {
        inputProps: {
          value,
          onChange
        },
        isValid
      }
    }
    

    So, right here we're having a function that has 2 parameters, validationRule and initialValue(which is optional and will default to text if nothing is provided).

    We're doing the basic value / onChange stuff, and then we're returning those 2 as inputProps. Besides, we're just calling the validationRule (beforehand, we check that it exists and it's sent as parameter).

    How to use:

    export default function SomeForm() {
      const { inputProps, isValid } = useInput((value) => value.includes('@'));
      
      return <Input {...inputProps}/>;
    }
    

    The following part is something that I strongly discourage. This is bad but currently, the only way of seeing it implemented with refs is using an useReducer that would force an update onChange.

    Eg:

    const useInput = (validationRule) => {
      const [, forceUpdate] = useReducer((p) => !p, true);
      const inputRef = useRef();
    
      const onChange = () => forceUpdate();
    
      const isValid = validationRule && validationRule(inputRef.current?.value);
    
      return {
        inputRef,
        isValid,
        onChange
      };
    };
    

    Then, used as:

    export default function SomeForm() {
      const { inputRef, onChange, isValid } = useInput((value) =>
        value && value.includes("@")
      );
      console.log(isValid);
    
      return <Input ref={inputRef} onChange={onChange} />;
    }