Home > Software engineering >  Transform this React snippet to Typescript?
Transform this React snippet to Typescript?

Time:08-15

Hi I am labbing with this code snippet and want to use it in Typescript but it complains about the useRef type and many more. Most importantly: it doesn't allow the next.focus();. It just says "Property 'focus' does not exist on type 'never'.ts(2339)"

Is it "not ok" in typescript to use the .focus() or do it want me to use onFocus or something?

The link to the Sandbox: https://codepen.io/vitalyq/pen/GRNXmQY?editors=0011

Here's the code in just React:

function useFocusNext() {
  const controls = useRef([]);

  const handler = (event) => {
    if (event.keyCode === 13) {

      const index = controls.current.indexOf(event.target);
      const next = controls.current[index   1];
      next && next.focus();

      // IE 9, 10
      event.preventDefault();
    }
  };

  return useCallback((element) => {
    if (element && !controls.current.includes(element)) {
      controls.current.push(element);
      element.addEventListener('keydown', handler);
    }
  }, []);
};

function Form() {
  const focusNextRef = useFocusNext();
  const [showMore, setShowMore] = useState(false);
  const toggleMore = () => setShowMore((last) => !last);

  return (
    <>
      <input ref={focusNextRef} placeholder="Field 1" />
      {showMore && <>
        <input ref={focusNextRef} placeholder="Added Field" />
      </>}
      <input ref={focusNextRef} placeholder="Field 2" />
      <button ref={focusNextRef}>Submit</button>
      <button onClick={toggleMore}>Toggle More Fields</button>
    </>
  );
};

ReactDOM.render(
  <Form />,
  document.getElementById('root')
);

And here's my code trying to transform it

  const useFocusNext = () => {
    const controls: MutableRefObject<never[]> = useRef([]);

    const handler = (event: {
      keyCode: number;
      target: any;
      preventDefault: () => void;
    }) => {
      if (event.keyCode === 13) {
        if(event && event.target) {
          const index: number = controls.current.indexOf(event.target as never);
          const next = controls.current[index   1];
          if (next) {
            next.focus();
          }
        }

        // IE 9, 10
        event.preventDefault();
      }
    };

    return useCallback((element) => {
      if (element && !controls.current.includes(element as never)) {
        controls.current.push(element as never);
        element.addEventListener("keydown", handler);
      }
    }, []);
  };

CodePudding user response:

You probably want to have an array of HTMLElement so:

useRef<HTMLElement[]>([])

CodePudding user response:

The never issue is that you're telling TypeScript that the elements in controls.current will never exist; that's what the never type says. Your controls will be HTMLElement instances, so use HTMLElement[] instead.

But the next problem is that you need a type on the element parameter of the function you return from useFocusNext, so TypeScript knows what values you're allowed to call it with. Also, in your handler function, event implicitly has the type any (I recommend enabling noImplicitAny so TypeScript flags this up); you'll want to say specifically it's a KeyboardEvent. That would also have allowed your IDE to flag up that keyCode is deprecated and shouldn't be used in new code; use key instead.

See the indicated portions below:

function useFocusNext() {
    const controls = useRef<HTMLElement[]>([]);
    // −−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^

    const handler = (event: KeyboardEvent) => {
        // −−−−−−−−−−−−−−−^^^^^^^^^^^^^^^
        if (event.key === "Enter") {
            //   ^^^^^^^^^^^^^^^^
            const index = controls.current.indexOf(event.target as HTMLElement);
            // −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^
            const next = controls.current[index   1];
            if (next) {
                next.focus();
            }

            // IE 9, 10
            event.preventDefault();
        }
    };

    return useCallback((element: HTMLElement | null) => {
    // −−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^
        if (element && !controls.current.includes(element)) {
            controls.current.push(element);
            element.addEventListener("keydown", handler);
        }
    }, []);
}

(No changes to Form were required.)


That said, I would avoid having code adding event listeners it never removes, and adding event listeners to each and every element in the form. Instead, I'd use a single event listener on the form, something along these lines:

function useFocusNext() {
    const formRef = useRef<HTMLFormElement>(null);

    useEffect(() => {
        const form = formRef.current;
        if (!form) {
            return;
        }
        // assert(form);
        const handler = (event: KeyboardEvent) => {
            if (event.key === "Enter" && event.target) {
                event.preventDefault();
                const elements = Array.from(form.querySelectorAll(".handle-next")) as HTMLElement[];
                const index = elements.indexOf(event.target as HTMLElement);
                const next = elements[index   1];
                if (next) {
                    next.focus();
                }
            }
        };
        form.addEventListener("keydown", handler);
        return () => {
            form.removeEventListener("keydown", handler);
        }
    }, []);

    return formRef;
}

function Form(props: any) {
    const formRef = useFocusNext();
    const [showMore, setShowMore] = useState(false);
    const toggleMore = () => setShowMore((last) => !last);

    return (
        <form {...props} ref={formRef}>
            <input className="handle-next" placeholder="Field 1" />
            {showMore && (
                <input className="handle-next" placeholder="Added Field" />
            )}
            <input className="handle-next" placeholder="Field 2" />
            <button className="handle-next">Submit</button>
            <button onClick={toggleMore}>Toggle More Fields</button>
        </form>
    );
}

That adds a form element around the form and handles events on it; the fields you want the Enter handled on are indicated with a class. (Or it could be a data-* attribute or something.) And the event handler is cleaned up when the component instance using it is unmounted.

Live Example:

const { useState, useRef, useEffect } = React;

function useFocusNext() {
    const formRef = useRef/*<HTMLFormElement>*/(null);

    useEffect(() => {
        const form = formRef.current;
        if (!form) {
            return;
        }
        // assert(form);
        const handler = (event/*: KeyboardEvent*/) => {
            if (event.key === "Enter" && event.target) {
                event.preventDefault();
                const elements = Array.from(form.querySelectorAll(".handle-next"))/* as HTMLElement[]*/;
                const index = elements.indexOf(event.target/* as HTMLElement*/);
                const next = elements[index   1];
                if (next) {
                    next.focus();
                }
            }
        };
        form.addEventListener("keydown", handler);
        return () => {
            form.removeEventListener("keydown", handler);
        }
    }, []);

    return formRef;
}

function Form(props: any) {
    const formRef = useFocusNext();
    const [showMore, setShowMore] = useState(false);
    const toggleMore = () => setShowMore((last) => !last);

    return (
        <form {...props} ref={formRef}>
            <input className="handle-next" placeholder="Field 1" />
            {showMore && (
                <input className="handle-next" placeholder="Added Field" />
            )}
            <input className="handle-next" placeholder="Field 2" />
            <button className="handle-next">Submit</button>
            <button onClick={toggleMore}>Toggle More Fields</button>
        </form>
    );
}

const Example = () => {
    return <Form/>;
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

That code has to have a couple of type assertions to tell TypeScript that we know we're dealing with HTMLElement instances, not just Element instances, since querySelectorAll returns a NodeListOf<Element> and event.target's type is just EventTarget. If you find yourself having to query HTML elements often, you'd probably be better off with a utility function with a more specific return type, and maybe even a subtype of KeyboardEvent that refines the type of target.

  • Related