I am trying to develop a click event handler function for the DOM element so that when I click to the outside of the div the corresponding dom element close. I have been trying the following code but I am getting the error of TypeError: node.contains is not a function. Not sure if I am doing it correctly with the react hook. Any kinds of help would be really appreciated.
import React, { useState, useEffect, useRef } from 'react';
const OutSiderClickComponent = () => {
const [visible, setVisible] = useState(false);
const node = useRef();
const handleClick = () => {
if (!visible) {
document.addEventListener('click', handleOutsideClick, false);
} else {
document.removeEventListener('click', handleOutsideClick, false);
}
setVisible(prevState => ({
visible: !prevState.visible,
}));
}
const handleOutsideClick = (e) => {
if (node.contains(e.target)) {
return;
}
handleClick();
}
return(
<div ref={node}>
<button onClick={handleClick}>Click to See</button>
{visible && <div>You Clicked the Button</div>}
</div>
);
};
export default OutSiderClickComponent;
When you use useRef you need to remember that the value is in current attribute of ref.
Try node.current.contains().
The rest should look more like that, using React.useEffect:
const handleOutsideClick = (e) => {
if (node.current.contains(e.target)) {
console.log('clicked inside');
// this.setVisible(true);
} else {
this.setVisible(false);
}
}
React.useEffect(() => {
document.addEventListener('click', handleOutsideClick, false);
return () => void document.removeEventListener('click', handleOutsideClick, false);
}, []);
and
<button onClick={() => void setVisible(true)}>Click to See</button>
There are two changes. First, you need to use node.current to check for the ref,
node.current.contains(e.target) . Also the ref must be attached to the node to which you need to detect outside click
var { useState, useEffect, useRef } = React;
const OutSiderClickComponent = () => {
const [visible, setVisible] = useState(false);
const node = useRef();
const handleClick = () => {
if (!visible) {
document.addEventListener('click', handleOutsideClick, false);
} else {
document.removeEventListener('click', handleOutsideClick, false);
}
setVisible(prevState => ({
visible: !prevState.visible,
}));
}
const handleOutsideClick = (e) => {
if (node.current.contains(e.target)) {
return;
}
setVisible(prev => !prev.visible)
}
return(
<div>
<button onClick={handleClick}>Click to See</button>
{visible && <div ref={node}>You Clicked the Button</div>}
</div>
);
};
ReactDOM.render(<OutSiderClickComponent />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="app" />
Related
I have met issues setting the state of the const displayClipName in the following function which is blocking despite the fact that I passed the props from the parent element to the child.
const audioClips = [
{
keyCode: 67,
keyTrigger: "C",
id: "Closed-HH",
src: "https://s3.amazonaws.com/freecodecamp/drums/Cev_H2.mp3"
}
]
function App() {
const [displayClipName, setDisplayClipName] = React.useState('Click a key!')
return (
<div id="drum-machine" className="text-white text-center">
<div className="container bg-info">
<h1>FCC - Drum Machine</h1>
{audioClips.map((clip) => (
<Pad
key={clip.id}
clip={clip}
setDisplayClipName={setDisplayClipName}
/>
))}
<h2>{displayClipName}</h2>
</div>
</div>
)
}
const Pad = ({clip, setDisplayClipName}) => {
const [playing, setPlaying] = React.useState(false)
React.useEffect(() => {
document.addEventListener('keydown', handleKey);
return () => {
document.removeEventListener('keydown', handleKey)
}
}, [])
const handleKey = (e) => {
if(e.keyCode === clip.keyCode) {
playSound()
}
}
const playSound = () => {
const audioPlay = document.getElementById(clip.keyTrigger);
const clipName = document.getElementById(clip.id)
setPlaying(true);
setTimeout(() => setPlaying(false), 300);
audioPlay.currentTime = 0;
audioPlay.play();
setDisplayClipName(clipName);
console.log(setDisplayClipName)
}
return (
<div onClick={playSound} id={`drum-pad-${clip.keyTrigger}`}>
<audio src={clip.src} className="clip" id={clip.keyTrigger}/>
{clip.keyTrigger}
</div>
)
}
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(<App />);
The console returns the following message:
function dispatchSetState()
length: 1
name: "bound dispatchSetState"
<prototype>: function ()
As some have pointed out in comments to your post it'd be better if you used refs. Also you were logging a function that's why the console displayed that. I have taken the liberty to do some modifications to your code so I could understand it better, I would suggest you keep the ones you find reasonable:
The displayName variable has an undefined state when no song is playing. This could be set from any other part of the application but you wouldn't depend on rerendering the component for it to return to a default message ("Press a key!")
The playSound function could be bound to the id of the song and you would avoid having to check the HTML element that received the input.
Here is a working snippet.
const { useState, useEffect } = React;
const audioClips = [
{
keyCode: 67,
keyTrigger: "C",
id: "Closed-HH",
src: "https://s3.amazonaws.com/freecodecamp/drums/Cev_H2.mp3"
}
]
const Pad = ({ clip, setDisplayClipName }) => {
const [playing, setPlaying] = useState(false);
useEffect(() => {
document.addEventListener("keydown", handleKey);
return () => {
document.removeEventListener("keydown", handleKey);
};
}, []);
const handleKey = (e) => {
if (e.keyCode === clip.keyCode) {
playSound(clip.id);
}
};
const playSound = (clipId) => {
const audioPlay = document.getElementById(clip.keyTrigger);
setPlaying(true);
setTimeout(() => setPlaying(false), 300);
audioPlay.currentTime = 0;
audioPlay.play();
setDisplayClipName(clipId);
};
return (
<div onClick={() => playSound(clip.id)} id={`drum-pad-${clip.keyTrigger}`}>
<audio src={clip.src} className="clip" id={clip.keyTrigger} />
{clip.keyTrigger}
</div>
);
};
function App() {
const [clipName, setClipName] = useState(undefined);
return (
<div id="drum-machine" className="text-white text-center">
<div className="container bg-info">
<h1>FCC - Drum Machine</h1>
{audioClips.map((clip) => (
<Pad key={clip.id} clip={clip} setDisplayClipName={setClipName} />
))}
<h2>{clipName ? clipName : "Press a key!"}</h2>
</div>
</div>
);
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<App />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
To further refine the Pad component and avoid the missing dependencies on the useEffect hook I would suggest you model it like this, using useMemo:
export const Pad = ({ clip, setDisplayClipName }) => {
const [playing, setPlaying] = useState(false);
const playSound = useMemo(
() => () => {
const audioPlay = document.getElementById(clip.keyTrigger);
setPlaying(true);
setTimeout(() => setPlaying(false), 300);
if (audioPlay) {
audioPlay.currentTime = 0;
audioPlay.play();
}
setDisplayClipName(clip.id);
},
[setDisplayClipName, clip]
);
useEffect(() => {
const handleKey = (e) => {
if (e.keyCode === clip.keyCode) {
playSound();
}
};
document.addEventListener("keydown", handleKey);
return () => {
document.removeEventListener("keydown", handleKey);
};
}, [playSound, clip.keyCode]);
return (
<div onClick={playSound} id={`drum-pad-${clip.keyTrigger}`}>
<audio src={clip.src} className="clip" id={clip.keyTrigger} />
{clip.keyTrigger}
</div>
);
};
I have the following React example, where I'm setting a state to the value of another state setter.
import React, {
Dispatch,
SetStateAction,
useEffect,
useState,
} from "react";
import ReactDOM from "react-dom";
const A = ({}) => {
const [setStage, setStageSetter] = useState<Dispatch<SetStateAction<number>>();
useEffect(() => {
console.log('A.setStage', setStage);
if (setStage) {
setStage(2);
}
}, [setStage, setStageSetter]);
return <B setStageSetter={setStageSetter} />;
};
const B = ({ setStageSetter }) => {
const [stage, setStage] = useState<number>(1);
useEffect(() => {
console.log('B.setStage', setStage);
setStageSetter(setStage);
}, [setStage, setStageSetter]);
return <p>{stage}</p>;
};
ReactDOM.render(<A />, mountNode);
However, when I run the above, it does not output what I'd expect, which is for the above to read 2 in the DOM. It seems to set the value to 1, and then to undefined Why is this?
A solution to the above is the following answer:
const { useEffect, useState } = React;
const B = ({ setStageSetter }) => {
const [stage, setStage] = useState(1);
useEffect(() => {
setStageSetter(() => setStage);
}, [setStage, setStageSetter]);
return <p>{stage}</p>;
};
const A = () => {
const [setStage, setStageSetter] = useState();
useEffect(() => {
if (setStage) {
setStage(2);
}
}, [setStage]);
return <B setStageSetter={setStageSetter} />;
};
ReactDOM.render(<A />, mountNode);
The functional difference is from setStageSetter(setStage); to setStageSetter(() => setStage);
Whilst this does set the parent's state to the setter of the child, and I can call a method in the parent and set the child's state, I don't understand why the callback is required. Why is this? Is this "bad practice"?
When the setStageSetter call was invoked with a different parameter, it seems to have achieved the desired objective. Please see the code snippet below for a working demo.
Code Snippet
const {
Dispatch,
SetStateAction,
useEffect,
useState
} = React;
const B = ({ setStageSetter }) => {
const [stage, setStage] = useState(1);
// <Number>
useEffect(() => {
console.log('B stage: ', stage, ' setStage', setStage);
// calling the props method with a "function" and not the setter as-is
setStageSetter(() => setStage);
}, [setStage, setStageSetter]);
return <p>{stage}</p>;
};
const A = ({}) => {
const [setStageA, setStageSetter] = useState(()=>{});
// <Dispatch<SetStateAction<Number>>
useEffect(() => {
console.log('A setStageA', setStageA);
if (setStageA) { // to realize the change on screen, delay by 1.5 seconds
setTimeout(() => setStageA(2), 1500);
// setStageA(2); // this works too, but change is not immediately observable
}
}, [setStageA, setStageSetter]);
return <B setStageSetter={setStageSetter} />;
};
ReactDOM.render(
<div>
DEMO
<A />
</div>,
document.getElementById("rd")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="rd" />
Explanation
Inline comments added to the snippet above.
I´m building some modals for fun but I can´t make it work for multiple modals.
useModal hook
import { useState } from 'react';
const useModal = () => {
const [isShowing, setIsShowing] = useState(false);
const toggle = () => {
setIsShowing(!isShowing);
}
return {
isShowing,
toggle,
}
};
export default useModal;
Modal component
import React, { useEffect } from 'react';
const Modal = (props) => {
const { toggle, isShowing, children } = props;
useEffect(() => {
const handleEsc = (event) => {
if (event.key === 'Escape') {
toggle()
}
};
if (isShowing) {
window.addEventListener('keydown', handleEsc);
}
return () => window.removeEventListener('keydown', handleEsc);
}, [isShowing, toggle]);
if (!isShowing) {
return null;
}
return (
<div className="modal">
<button onClick={ toggle } >close</button>
{ children }
</div>
)
}
export default Modal
in my Main component
If I do this the only modal in the page works fine
const { isShowing, toggle } = useModal();
...
<Modal isShowing={ isShowing } toggle={ toggle }>first modal</Modal>
but when I try to add another one it doesn´t work. it doesn´t open any modal
const { isShowingModal1, toggleModal1 } = useModal();
const { isShowingModal2, toggleModal2 } = useModal();
...
<Modal isShowing={ isShowingModal1 } toggle={ toggleModal1 }>first modal</Modal>
<Modal isShowing={ isShowingModal2 } toggle={ toggleModal2 }>second modal</Modal>
what I´m doing wrong? thank you
if you want to check it out please go to https://codesandbox.io/s/hopeful-cannon-guptw?fontsize=14&hidenavigation=1&theme=dark
Try that:
const useModal = () => {
const [isShowing, setIsShowing] = useState(false);
const toggle = () => {
setIsShowing(!isShowing);
};
return [isShowing, toggle];
};
then:
export default function App() {
const [isShowing, toggle] = useModal();
const [isShowingModal1, toggleModal1] = useModal();
const [isShowingModal2, toggleModal2] = useModal();
if you have multiple modal then dynamically inject <div> inside the page body and map modal to it.
to create div use,
var divTag = document.createElement("div");
divTag.setAttribute('id', 'modal');
document.getElementsByTagName('body')[0].appendChild(divTag);
document.getElementById('modal').innerHTML = modalHtML;
This should work.
I use the library react-use-modal, and
I'm trying to read the updated value of confirmLoading when inside the handleClick function.
handleClick does read the first value of confirmLoading defined when doing const [ confirmLoading, setConfirmLoading ] = useState(false), but never updates when I setConfirmLoading inside handleOk.
I don't understand what I'm doing wrong
import { Button, Modal as ModalAntd } from 'antd'
import { useModal } from 'react-use-modal'
export interface ModalFormProps {
form: React.ReactElement
}
export const ModalForm: React.FC = () => {
const [ confirmLoading, setConfirmLoading ] = useState(false)
const { showModal, closeModal } = useModal()
const handleOk = () => {
setConfirmLoading(true)
setTimeout(() => {
setConfirmLoading(false)
closeModal()
}, 1000)
}
const handleCancel = () => {
closeModal()
}
const handleClick = () => {
showModal(({ show }) => (
<ModalAntd
onCancel={handleCancel}
onOk={handleOk}
title='Title'
visible={show}
>
// the value of confirmLoading is always the one defined
// with useState at the beginning of the file.
<p>{confirmLoading ? 'yes' : 'no'}</p>
</ModalAntd>
))
}
return (
<div>
<Button onClick={handleClick}>
Open Modal
</Button>
</div>
)
}
This is happening because of closures. The component that you pass to showModal remembers confirmLoading and when you call function setConfirmLoading your component renders again and function handleClick is recreated. 'Old' handleClick and 'old' component in showModal know nothing about the new value in confirmLoading.
Try to do this:
export const ModalForm: React.FC = () => {
const { showModal, closeModal } = useModal();
const handleClick = () => {
showModal(({ show }) => {
const [ confirmLoading, setConfirmLoading ] = useState(false);
const handleOk = () => {
setConfirmLoading(true)
setTimeout(() => {
setConfirmLoading(false)
closeModal()
}, 1000)
};
const handleCancel = () => {
closeModal()
};
return (
<ModalAntd
onCancel={handleCancel}
onOk={handleOk}
title='Title'
visible={show}
>
// the value of confirmLoading is always the one defined
// with useState at the beginning of the file.
<p>{confirmLoading ? 'yes' : 'no'}</p>
</ModalAntd>
);
})
};
return (
<div>
<Button onClick={handleClick}>
Open Modal
</Button>
</div>
)
}
I am using a custom hook to detect outside clicks
const useClickOutside = (nodeElement, handler) => {
function handleClickOutside(event) {
if (nodeElement && !nodeElement.contains(event.target)) {
handler();
}
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
}
And I am calling it like this
const Modal = ({ ... }) => {
const modalRef = useRef(null);
console.log(modalRef.current) // null
useEffect(() => {
console.log(modalRef.current) // work fine here and display the dom element
}, [])
// here the hooks it is called with modalRef.current as null
useClickOutside(modalRef.current, () => {
dispatch(hideModal());
});
return (
<div className="pop-container">
<div className="pop-dialog" ref={modalRef}>
...
</div>
</div>
)
}
The problem is that my custom hooks useClickOutside is called with modalRef.current as null
And as you see in the useEffet hook the modalRef.current value is correct
However i can't call my custom hook there in useEffet otherwise i will get Uncaught Invariant Violation: Hooks can only be called inside the body of a function component
So how to solve this issue ?
Instead of passing ref.current, if you just pass ref, your code will work since ref.current will be mutated at its reference when ref is assigned
const useClickOutside = (nodeElement, handler) => {
function handleClickOutside(event) {
if (nodeElement.current && !nodeElement.current.contains(event.target)) {
handler();
}
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
}
and in modal
const Modal = ({ ... }) => {
const modalRef = useRef(null);
// here the hooks it is called with modalRef.current as null
useClickOutside(modalRef, () => {
dispatch(hideModal());
});
return (
<div className="pop-container">
<div className="pop-dialog" ref={modalRef}>
...
</div>
</div>
)
}
Working demo