-1

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

Lauro235
  • 121
  • 1
  • 8
  • **ref={ref} className="flex gap-2 bg-yellow-400"** the ref here is a htmldivelement not a ref you can deleted **className="flex gap-2 bg-yellow-400"** – Christa Aug 31 '23 at 20:42
  • Hi Christa, Thanks for your response. I don't understand. Are you telling me to delet the className text or the ref? – Lauro235 Aug 31 '23 at 20:43
  • delete de ref is not usefull – Christa Aug 31 '23 at 20:44
  • Are you saying that the typing is correct, but that my use of ref is incorrect? The element I've placed the ref on is the element I want to track in the react dom. Would you suggest using forwardRef? – Lauro235 Aug 31 '23 at 20:46
  • **ref={(element: HTMLDivElement) => noteRefs.current?.push(element)}** the noteRefs contain an array of html elements and not the reference of element – Christa Aug 31 '23 at 20:47
  • Yes your use of useRef is wrong you don't use it for an array you use it for an induvial element, not for multiple. – Jesse Aug 31 '23 at 20:49
  • I'm not sure I agree with you Jesse. There must be a way to generate refs dynamically no? – Lauro235 Aug 31 '23 at 20:50
  • Perhaps, I'm too close minded, I'll have a look, maybe you're right. – Jesse Aug 31 '23 at 20:53
  • you want when the user click outside ?
    or outside div of className="m-5 grid gap-5"
    – Christa Aug 31 '23 at 21:00
  • Hi Christa, yes, that's correct. Each Note component contains the 'task' which will either display as an input or as a p tag. When the user is in input mode, if they click out the task will display as a p tag. – Lauro235 Aug 31 '23 at 21:04

2 Answers2

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

Jesse
  • 334
  • 15
0

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>
  );
}
Christa
  • 398
  • 7