3
votes

I'm trying to follow atomic design principles, but I'm not sure if I'm doing it correctly. I have created three components: Form, ExpandableInput and Input. Input is HTML input wrapper, Expandable input is Input with dropdown and Form is just form. I want to access current input value from Form component so I'm doing ref forwarding by multiple levels. Everything works fine in Form component, but the problem is that I have to use ref.current.value in ExpandableInput component, but I'm getting following error.

Property 'current' does not exist on type '((instance: HTMLInputElement | null) => void) | MutableRefObject<HTMLInputElement | null>'. Property 'current' does not exist on type '(instance: HTMLInputElement | null) => void'.

I don't know how to tell typescript that ref variable in ExpandableInput has type of Ref and is not null. I don't know if ref forwarding by multiple levels is good practice, maybe there is a better way to get input value.

Example code: AddForm

const Form = () => {
    // Refs
    const caRef = useRef<HTMLInputElement>(null);

    return (
        <FormTemplate >
            <ExpandableInput
                option={CompetenceAreaOption}
                ref={caRef}

            />
        </FormTemplate>
    );
}

export default AddForm;

ExpandableInput

const ExpandableInput = React.forwardRef<HTMLInputElement, IProps>((
   props,
   ref
): JSX.Element => {

    const pickInputValue = (value: string): void => {
        console.log(ref.current) // Error
        console.log(ref) // Input reference
        }
    }

    return (
        <div>
            <div>
                <Input
                    ref={ref}
                />
            </div>             
        </div>
    );
})

export default ExpandableInput;      

Input

const Input = React.forwardRef<HTMLInputElement, IProps>(({
    props,
    ref
): JSX.Element => {
 
    return (
        <input
            ref={ref}
        />
    );
})


export default Input;
1
i think you shoud use context instead using ref forwarding - san
"I don't know how to tell typescript that ref variable in ExpandableInput has type of Ref and is not null" , so don't pass null to useRef, also you should make a sandbox your problem - Dennis Vash
If you want to access input value, don't use refs, use state, make your input controlled component - Alex Chashin

1 Answers

0
votes

There are actually three possible values for the ref received by forwardRef.

type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;

It can be null. It can be what you are expecting, which is a MutableRefObject created by useRef. And the third possibility is that it can be a callback function. (Note: legacy string refs are not supported by forwardRef).

Any of these three are fine to pass down to the next level. But if we want to access ref.current then we need to make sure that what we have is a MutableRefObject. We do this with a type guard: if ( ref && "current" in ref). That makes it ok to access ref.current.

ref.current might be null (when the ref hasn't been attached to the DOM yet). So we also need to check that current is valid either with an if or with the optional chaining operator ?.

Short version:

if ( ref && "current" in ref) {
  ref.current?.focus();
}

Long version:

// make sure that ref is a MutableRefObject
if (ref && "current" in ref) {
    // input has type HTMLInputElement | null
    const input = ref.current;
    // exlude null current value
    if (input) {
        // now we know that we have an input
        input.focus();
    }
}

Typescript Playground Link