skip to Main Content

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

2

Answers


  1. Try this code:

    import React, { useState, useEffect, useRef, useCallback } from "react";
    
    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;
    }
    
    const list = [
      {
        id: "1a1",
        task: "wash dishes",
        status: true,
      },
      {
        id: "7bs",
        task: "cook dinner",
        status: false,
      },
      {
        id: "45q",
        task: "Study",
        status: true,
      },
    ];
    
    function App() {
      const noteRefs = useRef<(HTMLDivElement | null)[]>([]);
      const [toDos, setTodos] = useState<ITask[]>(list);
    
      useEffect(() => {
        noteRefs.current = noteRefs.current.slice(0, toDos.length);
      }, [toDos]);
    
      return (
        <div className="bg-blue-500 h-screen">
          <h2>To do</h2>
          {toDos.map((note, index) => {
            return (
              <div
                className="m-5 grid gap-5"
                key={note.id}
              >
                <ForwardedNote
                  note={note}
                  index={index}
                  toDos={toDos}
                  setToDos={setTodos}
                  ref={element => (noteRefs.current[index] = element)}
                />
              </div>
            );
          })}
        </div>
      );
    }
    
    interface INoteProps {
      note: ITask;
      toDos: ITask[];
      setToDos: React.Dispatch<React.SetStateAction<ITask[]>>;
      index: number;
    }
    
    function Note(
      { note, toDos, setToDos, index }: INoteProps,
      ref: React.ForwardedRef<HTMLDivElement>
    ) {
      const [open, setOpen] = useState(false);
    
      const handleClickOutside = useCallback(
        (e: MouseEvent) => {
          console.log("clicking anywhere");
          assertIsNode(e.target);
          if (ref?.current?.contains(e.target)) {
            console.log("clicked inside!");
            return;
          }
          console.log(open);
          setOpen(false);
        },
        [open, ref]
      );
    
      useEffect(() => {
        if (open) {
          document.addEventListener("mousedown", handleClickOutside);
        } else {
          document.removeEventListener("mousedown", handleClickOutside);
        }
    
        return () => {
          document.removeEventListener("mousedown", handleClickOutside);
        };
      }, [open, handleClickOutside]);
    
      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} readOnly />
          <input
            checked={note.status}
            onChange={inputHandler}
            type="checkbox"
            readOnly
          />
        </div>
      );
    }
    
    const ForwardedNote = React.forwardRef<HTMLDivElement, INoteProps>(Note);
    
    export default App;
    
    

    Output

    import React, { useState, useEffect, useRef, useCallback } from "react";
    function assertIsNode(e) {
        if (!e || !("nodeType" in e)) {
            throw new Error(`Node expected`);
        }
    }
    const list = [
        {
            id: "1a1",
            task: "wash dishes",
            status: true,
        },
        {
            id: "7bs",
            task: "cook dinner",
            status: false,
        },
        {
            id: "45q",
            task: "Study",
            status: true,
        },
    ];
    function App() {
        const noteRefs = useRef([]);
        const [toDos, setTodos] = useState(list);
        useEffect(() => {
            noteRefs.current = noteRefs.current.slice(0, toDos.length);
        }, [toDos]);
        return (React.createElement("div", { className: "bg-blue-500 h-screen" },
            React.createElement("h2", null, "To do"),
            toDos.map((note, index) => {
                return (React.createElement("div", { className: "m-5 grid gap-5", key: note.id },
                    React.createElement(ForwardedNote, { note: note, index: index, toDos: toDos, setToDos: setTodos, ref: element => (noteRefs.current[index] = element) })));
            })));
    }
    function Note({ note, toDos, setToDos, index }, ref) {
        const [open, setOpen] = useState(false);
        const handleClickOutside = useCallback((e) => {
            var _a;
            console.log("clicking anywhere");
            assertIsNode(e.target);
            if ((_a = ref === null || ref === void 0 ? void 0 : ref.current) === null || _a === void 0 ? void 0 : _a.contains(e.target)) {
                console.log("clicked inside!");
                return;
            }
            console.log(open);
            setOpen(false);
        }, [open, ref]);
        useEffect(() => {
            if (open) {
                document.addEventListener("mousedown", handleClickOutside);
            }
            else {
                document.removeEventListener("mousedown", handleClickOutside);
            }
            return () => {
                document.removeEventListener("mousedown", handleClickOutside);
            };
        }, [open, handleClickOutside]);
        function inputHandler() {
            setToDos(toDos.map(task => {
                return task ? Object.assign(Object.assign({}, task), { status: !task.status }) : task;
            }));
        }
        return (React.createElement("div", { ref: ref, className: "flex gap-2 bg-yellow-400" },
            React.createElement("input", { className: "text-black", type: "text", value: note.task, readOnly: true }),
            React.createElement("input", { checked: note.status, onChange: inputHandler, type: "checkbox", readOnly: true })));
    }
    const ForwardedNote = React.forwardRef(Note);
    export default App;
    
    

    Compiler Options

    {
      "compilerOptions": {
        "strict": true,
        "noImplicitAny": true,
        "strictNullChecks": true,
        "strictFunctionTypes": true,
        "strictPropertyInitialization": true,
        "strictBindCallApply": true,
        "noImplicitThis": true,
        "noImplicitReturns": true,
        "alwaysStrict": true,
        "esModuleInterop": true,
        "declaration": true,
        "target": "ES2017",
        "jsx": "react",
        "module": "ESNext",
        "moduleResolution": "node"
      }
    }
    

    Playground Link: Provided

    Login or Signup to reply.
  2. 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 :

    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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search