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
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
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>
);
}