Search code examples
reactjstypescriptreact-hooksref

Typing for an Array of Refs


I'm trying to create a component that allows for the user to click outside it. To do this I need to create a ref for each component that requires the functionality. This is what I'm trying to type for.

I'm struggling to find a fix for the error HTMLDivElement | null is not assignable to type Legacy<HTMLDivElement> | undefined. I've looked through the SO thread here and I'm still not having any success.

useRef TypeScript - not assignable to type LegacyRef<HTMLDivElement>

Here is my code currently. Please see line 127 => https://tsplay.dev/mxypbW


Solution

  • This is how you solve you problem because you are adding unnecessary code and you can delete noteRefs and access to the parent HTML element from the child using ref?.current?.parentElement :

    enter image description here

    import React, {
      Dispatch,
      SetStateAction,
      useCallback,
      useEffect,
      useRef,
      useState
    } from "react";
    import "./styles.css";
    
    function assertIsNode(e: EventTarget | null): asserts e is Node {
      if (!e || !("nodeType" in e)) {
        throw new Error(`Node expected`);
      }
    }
    
    export interface ITask {
      id?: string;
      task?: string;
      status?: boolean;
    }
    
    interface INoteProps {
      note: ITask;
      toDos: ITask[];
      setToDos: Dispatch<SetStateAction<ITask[]>>;
    }
    
    const list = [
      {
        id: "1a1",
        task: "wash dishes",
        status: true
      },
      {
        id: "7bs",
        task: "cook dinner",
        status: false
      },
      {
        id: "45q",
        task: "Study",
        status: true
      }
    ];
    
    const Note = ({ note, toDos, setToDos }: INoteProps) => {
      const [open, setOpen] = useState(true);
      const ref = useRef<HTMLDivElement>(null);
    
      const handleClickOutside = useCallback(
        (e: MouseEvent) => {
          console.log("clicking anywhere");
          assertIsNode(e.target);
          if (ref?.current?.parentElement?.contains(e.target)) {
            // inside click
            console.log("clicked!");
    
            return;
          }
          console.log(open);
    
          // outside click
          setOpen(false);
        },
        [open]
      );
    
      useEffect(() => {
        if (open) {
          document.addEventListener("mousedown", handleClickOutside);
        } else {
          document.removeEventListener("mousedown", handleClickOutside);
        }
    
        return () => {
          document.removeEventListener("mousedown", handleClickOutside);
        };
      }, [open, handleClickOutside]);
    
      // note comes from the note object mapped in app
      // const [input, setInput] = useState<string | undefined>(note.task);
    
      function inputHandler() {
        setToDos(
          toDos.map((task) => {
            return task ? { ...task, status: !task.status } : task;
          })
        );
      }
    
      return (
        <div ref={ref} className="flex gap-2 bg-yellow-400">
          <input
            className="text-black"
            type="text"
            value={note.task}
            onChange={() => console.log("")}
          />
          <input checked={note.status} onChange={inputHandler} type="checkbox" />
        </div>
      );
    };
    
    export default function App() {
      const [toDos, setTodos] = useState<ITask[]>(list);
      return (
        <div className="bg-blue-500 h-screen">
          <h2>To do</h2>
          {/* <NoteForm inputHandler={inputHandler} setTodos={setTodos} /> */}
          {toDos.map((note, index) => {
            return (
              <div className="m-5 grid gap-5" key={note.id}>
                <Note note={note} toDos={toDos} setToDos={setTodos} />
              </div>
            );
          })}
        </div>
      );
    }
    

    you can check the sandbox :

    https://codesandbox.io/s/react-typescript-forked-4xgw9z?file=/src/App.tsx:0-2556

    or more clean simple way :

    remove div wrapper from :

    return (
          <Note key={note.id} note={note} toDos={toDos} setToDos={setTodos} />
        );
    

    and add it inside the note component just like so :

    import React, {
      Dispatch,
      SetStateAction,
      useCallback,
      useEffect,
      useRef,
      useState
    } from "react";
    import "./styles.css";
    
    function assertIsNode(e: EventTarget | null): asserts e is Node {
      if (!e || !("nodeType" in e)) {
        throw new Error(`Node expected`);
      }
    }
    
    export interface ITask {
      id?: string;
      task?: string;
      status?: boolean;
    }
    
    interface INoteProps {
      note: ITask;
      toDos: ITask[];
      setToDos: Dispatch<SetStateAction<ITask[]>>;
    }
    
    const list = [
      {
        id: "1a1",
        task: "wash dishes",
        status: true
      },
      {
        id: "7bs",
        task: "cook dinner",
        status: false
      },
      {
        id: "45q",
        task: "Study",
        status: true
      }
    ];
    
    const Note = ({ note, toDos, setToDos }: INoteProps) => {
      const [open, setOpen] = useState(false);
      const ref = useRef<HTMLDivElement>(null);
    
      const handleClickOutside = useCallback(
        (e: MouseEvent) => {
          console.log("clicking anywhere");
          assertIsNode(e.target);
          if (ref?.current?.contains(e.target)) {
            // inside click
            console.log("clicked!");
    
            return;
          }
          console.log(open);
    
          // outside click
          setOpen(false);
        },
        [open]
      );
    
      useEffect(() => {
        if (open) {
          document.addEventListener("mousedown", handleClickOutside);
        } else {
          document.removeEventListener("mousedown", handleClickOutside);
        }
    
        return () => {
          document.removeEventListener("mousedown", handleClickOutside);
        };
      }, [open, handleClickOutside]);
    
      // note comes from the note object mapped in app
      // const [input, setInput] = useState<string | undefined>(note.task);
    
      function inputHandler() {
        setToDos(
          toDos.map((task) => {
            return task ? { ...task, status: !task.status } : task;
          })
        );
      }
    
      return (
        <div ref={ref} className="m-5 grid gap-5">
          {!open ? (
            <div onClick={() => setOpen(true)}> {note.task} </div>
          ) : (
            <div className="flex gap-2 bg-yellow-400">
              <input
                className="text-black"
                type="text"
                value={note.task}
                onChange={() => console.log("")}
              />
              <input
                checked={note.status}
                onChange={inputHandler}
                type="checkbox"
              />
            </div>
          )}
        </div>
      );
    };
    
    export default function App() {
      const [toDos, setTodos] = useState<ITask[]>(list);
      return (
        <div className="bg-blue-500 h-screen">
          <h2>To do</h2>
          {/* <NoteForm inputHandler={inputHandler} setTodos={setTodos} /> */}
          {toDos.map((note, index) => {
            return (
              <Note key={note.id} note={note} toDos={toDos} setToDos={setTodos} />
            );
          })}
        </div>
      );
    }