So I have a React state variable const [pickingHotspot, setPickingHotspot] = useState(false);. I then have this button <button type="button" className="btn btn-outline-danger" onClick={() => setPickingHotspot(true)}> which just sets the state to true onClick. I have another handler
tmp.on('mousedown', (event) => {
if (pickingHotspot){
console.log(tmp.mouseEventToCoords(event));
} else {
console.log(pickingHotspot);
}
});
where tmp is a Pannellum 360 Image Viewer (its a third party library, but I don't think it matters what it is), and this is set in my useState(...,[]) which runs once on load. Lastly, I have an onClick div that just prints the value of pickingHotspot for debugging purposed. Here's the weird part:
When I load the page and click the debug div, the value is false. Cool, that works. Then I click the button (which should set it to true!) and then click the debug div again. The value is true! But when I click the Pannellum viewer, the value is false? I'm not sure how the value could possibly be both true and false, depending on where I click. Are there different versions/instances of these variables? I've tried linking everything to individual function handlers that are outside of the html components and outside of the useEffect in case there's some weird scope stuff happening, but nothing has worked so far.
I tried to show all of the code needed, but here's the full thing (I took most of the unrelated stuff out to simplify it, its a lot of code to look through.):
function TourCreator(props){
const [scenes, setScenes] = useState({});
const [media, setMedia] = useState({});
const [viewer, setViewer] = useState(null);
// Editor states
const [pickingHotspot, setPickingHotspot] = useState(false);
function handle(event){
if (pickingHotspot){
console.log(viewer.mouseEventToCoords(event));
} else {
console.log(pickingHotspot);
}
}
// Called once on load
useEffect(() => {
if (Object.keys(media).length == 0){
// Sends the request to the backend for "data"
sendGetRequest(window.$PROJECT, true, {
id: params["project_id"],
}).then((data) => {
data.images = reshapeArray(data.images, 3);
console.log(data)
setMedia(data);
let tmp = window.pannellum.viewer('panorama', tour)
setViewer(tmp);
// Print Pitch/Yaw on click
tmp.on('mousedown', (event) => handle(event));
});
}
}, [])
return (
<div className="p-3">
{/* MAIN CONTENT */}
<div className='tour-creator-root mx-auto p-3 row rounded'>
{/* MAIN BOX */}
<div className='main-box col-9 px-0 rounded'>
{/* Pannellum viewer */}
<div id='panorama' className="w-100 rounded-top">
<button type="button" className="save-button btn btn-outline-danger">
Save
</button>
</div>
{/* Toolbar */}
<div className="toolbar w-100 d-flex flex-column justify-content-center rounded-bottom p-3"
onClick={
() => {
console.log(pickingHotspot)
}
}>
<div className="d-flex flex-row justify-content-around">
<button type="button" className="btn btn-outline-danger" onClick={
() => setPickingHotspot(true)
}>
Add Hotspot
</button>
</div>
</div>
</div>
</div>
</div>
)
}
export default TourCreator;
Try passing pickingHotspot in your dependency array for useEffect.
Your event handler is attached to your element in the useEffect on componentDidMount because of the empty dependency array. This will only happen once and that old function will be used. That old function will close over the value of the previous state. You can attach your event handler again on every relevant state change by passing pickHotSpot in your dependency array.
It is also a recommended approach to keep all your relevant code inside the hook. You could have put your listener function inside your hook, and would have seen a missing dependency warning from one of your lint tools.
Also, if there is no specific reason for you to add event hanlder like this from javascript, then add inline usin JSX, like #MB__ suggested. That will be executed on every render so it should be correct. At any time only one eventhandler for the particular event will be attached.
Related
i want to improve my code, with several buttons that has custom class names (attr), when clicked should add to body tag (toggle), now is adding the first button only because for ("button")[0] but should work for each button
import React, { useState, useEffect } from "react"
function Test() {
const [isClass, setIsClass] = useState(false)
useEffect(() => {
const x = document.getElementsByTagName("button")[0].getAttribute("custom-class")
document.body.classList.toggle(x, isClass)
}, [isClass])
return (
<>
<button custom-class='test1' onClick={() => setIsClass(!isClass)}>
Setting test1 className
</button>
<button custom-class='test2' onClick={() => setIsClass(!isClass)}>
Setting test2 className
</button>
</>
)
}
export default Test
Thanks
Please use this code.
let oldStyle = "";
const handleClick = (index) => {
const x = [...document.getElementsByTagName("button")].map(value => value.getAttribute("custom-class"));
document.body.classList.contains(x[index]) ? document.body.classList.remove(x[index]) : document.body.classList.add(x[index]);
if(document.body.classList.length > 1) document.body.classList.replace(oldStyle, x[index]);
oldStyle = x[index];
}
return (
<>
<button custom-class='test1' onClick={() => handleClick(0)}>
Setting test1 className
</button>
<button custom-class='test2' onClick={() => handleClick(1)}>
Setting test2 className
</button>
</>
)
It is better not to use DOM querying and manipulation directly with elements that are created and controlled by react. In your particular example it is ok to use document.body, but not ok to search for buttons, especially when you try to find them by tag name. To actually toggle a class in classList you don't need second parameter in most cases, so additional state is also not needed.
React way to get reference to element renderend by React would be to use Ref. However, in your particular case side effect can be launched inside event handler, so you don't need useEffect or useRef.
Your onClick handler can accept event object that is Synthetic Event. It holds property target that holds reference to your button.
So, the easiest way would be simply to write like this:
function Test() {
function clickHandler(event) {
let classToToggle = event.target.getAttribute("custom-class");
document.body.classList.toggle(classToToggle);
}
return (
<>
<button key="test1" custom-class="test1" onClick={clickHandler}>
Setting test1 className
</button>
<button key="test2" custom-class="test2" onClick={clickHandler}>
Setting test2 className
</button>
</>
);
}
export default Test;
If you need to have only single className from the list, you can decide which class to enable or disable with a bit of a state. Since anything can add classes on body it might be useful to operate only on some set of classes and not remove everything.
Also, not mentioned before, but consider using data attribute as its purpose is to keep some additional data.
function Test() {
// this can come from props or be hardcoded depending on your requirements
// If you intend to change it in runtime, consider adding side effect to cleanup previous classes on body
let [classesList] = React.useState(["test1", "test2"]);
let [activeClass, setActiveClass] = React.useState("");
// You can switch actual classes in effect, if you want to
function clickHandler(event) {
let classToToggle = event.target.dataset.customClass;
// we remove all classes from body that are in our list
document.body.classList.remove(...classesList);
if (activeClass === classToToggle) {
setActiveClass("");
} else {
// if class not active - set new one
document.body.classList.add(classToToggle);
setActiveClass(classToToggle);
}
}
return (
<>
{classesList.map((cn) => (
<button key="cn" data-custom-class={cn} onClick={clickHandler}>
Setting {cn} className
</button>
))}
</>
);
}
So I have two .js files (are they also called modules?). The first .js file is a class-based component. It has handleClick() as well as render(). It looks like this (I've actually removed a lot of the code to make it appear shorter here):
handleClick(event) {
event.preventDefault()
console.log('handleclick')
this.initializeFetchApiAndSetState()
}
//Helper Function
checkGuessForCorrectAnswer() {
console.log('correct answer!')
}
render() {
return (
<div className="container-main">
<MultipleChoices
onClick={this.handleClick}
data={this.state.guess1}
/>
<button
className='Submit'
onClick={this.handleClick}
>
Submit
</button>
</div>
)
}
The button above works fine in that I can click on it and it'll console log the word 'correct answer!'. But for some reason, when I try to pass onClick to the "MultipleChoices" file/module it doesn't console log 'correct answer!'. The MultipleChoices.js file looks like this:
import React from "react"
function MultipleChoices(props) {
return(
<div>
<div className="button-grid">
<button
className="btn"
value={props.data}
onClick={props.handleClick}
>
{props.data}
</button>
</div>
</div>
)
}
export default MultipleChoices
Why can the button activate onClick in the first file, but not when I try to pass onClick to the MultipleChoice.js (which also has a button)?
In your upper component, you need to replace the onClick property with a handleClick property.
<MultipleChoices
handleClick={this.handleClick}
data={this.state.guess1}
/>
Because inside the Multiple Choices component you are calling the handleClick method from the properties (which is not set)
In your parent component you have given name to your property as onClick, while you are trying to acces it in children component as prop.handleClick.
I am really wondering why getElementById is returing null, where as same element I am able to access using get elements by className. It will be great if somebody helps me to understand what is causing this.
Please find below code, its a functional react component where i am passing handleClose function as props, which closes the modal, it just not needed so i have put only Modal component code.
import React, { useRef, useEffect } from 'react'
const A11yModal = ({ handleClose }) => {
const focusClose = useRef(null)
var closeBtn_cls = document.getElementsByClassName("btn-close")
var closeIcon=document.getElementById('btnclose')
useEffect(() => {
focusClose.current.focus();
console.log("using Id",closeIcon);
console.log("using ClassName",closeBtn_cls);
}, [])
function onKeyPressed(e) {
if (e.keyCode === 27) {
handleClose()
}
}
return (
<div className="modal display-block">
<section className="modal-main" role="dialog"
aria-modal="true" onKeyDown={(e) => onKeyPressed(e)}>
<button className="btn-close" id="btnclose" onClick={(e) => handleClose(e)} ref={focusClose}>
X
</button>
<h1 id="modal_title" className="title" >Modal</h1>
<div id="full_description" className="description" aria-describedby="full_description">
<p>Description goes here.</p>
</div>
<button className="close_btn" id="closebtn" onClick={(e) => handleClose(e)}> Close </button>
</section>
</div>
)
}
export default A11yModal
var closeBtn_cls = document.getElementsByClassName("btn-close")
At the moment this line of code finishes running, closeBtn_cls will have nothing in it (assuming this is the first render and there is nothing else with that class name on the page). But closeBtn_cls is a live HTMLCollection. This array-like object has the peculiar property that it will be dynamically changed as elements are added to the DOM. So by the time the useEffect runs, the element has been added to the page and the collection updated.
getElementById does not return an HTMLCollection, so it does not update on the fly.
While that addresses the difference, you should also know that this is not the recommended way to do things in react. In react you should use refs to get a reference to the dom element. You seem to be aware of that, as you're used refs in your example, so i recommend just deleting the code that uses getElementsByClassName and getElementById.
Ok, so the reason that you see elements while using className is that on console the elements are evaluated when you expand objects which in this case is an HTMLCollection, so even though at the initial render there is no element present, after the execution of useEffect you will display the data into the console and by that the HTMLCollection is being initialized and the reference to the array is causes the values to be seen whereas while using the id you are directly returning a single element and there is no element present at the time of initial render.
const { useRef, useEffect } = React;
const A11yModal = ({ handleClose }) => {
const focusClose = useRef(null)
var closeBtn_cls = document.getElementsByClassName("btn-close")
var closeIcon=document.getElementById('btnclose')
console.log('class value', closeBtn_cls[0]);
useEffect(() => {
focusClose.current.focus();
console.log("using Id",closeIcon);
console.log("using ClassName",closeBtn_cls);
}, [])
function onKeyPressed(e) {
if (e.keyCode === 27) {
handleClose()
}
}
return (
<div className="modal display-block">
<section className="modal-main" role="dialog"
aria-modal="true" onKeyDown={(e) => onKeyPressed(e)}>
<button className="btn-close" id="btnclose" onClick={(e) => handleClose(e)} ref={focusClose}>
X
</button>
<h1 id="modal_title" className="title" >Modal</h1>
<div id="full_description" className="description" aria-describedby="full_description">
<p>Description goes here.</p>
</div>
<button className="close_btn" id="closebtn" onClick={(e) => handleClose(e)}> Close </button>
</section>
</div>
)
}
ReactDOM.render(<A11yModal />, 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" />
I was trying to resolve this problem, but I have no luck...
I'm using React and 'react-bootstrap'. Getting data from firebase with useState, as you can see in the next code. But also I'm calling a modal as a component, and this modal use useState to show and hide the modal.
export const Models = () => {
const [models, setModels] = useState(null);
useEffect(() => {
firebase.database().ref('Models').on('value', (snapshot) => {
setModels(snapshot.val())
});
}, []);
return models;
}
the problem result when I click on the url to access the modal, this one is shown and the main component goes to firebase and tries to get the data again. So, if I click 3 times on the modal, I will get my data from firebase 3 times.
How can I fix this? to get my data from firebase only one time, regardless of the times that you open the modal window?
The other part of the code
const Gallery = () => {
const [fireBaseDate, setFireBaseDate] = useState(null);
axios.post('https://us-central1-models-gallery-puq.cloudfunctions.net/date',{format:'DD/MM/YYYY'})
.then((response) => {
setFireBaseDate(response.data)
});
let content = Models();
let models = [];
const [imageModalShow, setImageModalShow] = useState(false);
const [selectedModel, setSelectedModel] = useState('');
if(content){
Object.keys(content).map((key, index) =>
models[index] = content[key]
);
models = shuffleArray(models);
console.log(models)
return(
<div className="appContentBody">
<Jumbo />
<Promotion models={models}/>
<div className="Gallery">
<h1>Galería - Under Patagonia</h1>
<Filter />
<div className="img-area">
{models.map((model, key) =>{
let myDate = new Date(model.creationDate);
let modelEndDate = new Date(myDate.setDate(myDate.getDate() + 30)).toLocaleDateString('en-GB')
if(fireBaseDate !== modelEndDate && model.active === true){
return (
<div className="img-card filterCard" key={key}>
<div className="flip-img">
<div className="flip-img-inner">
<div className="flip-img-front">
<img className="single-img card-img-top" src={model.thumbnail} alt="Model"/>
</div>
<div className="flip-img-back">
<h2>{model.certified ? 'Verificada!' : 'No Verificada'}</h2>
<p>Número: {model.contact_number}</p>
<p>Ciudad: {model.city}</p>
<p>Servicios: {model.services}</p>
</div>
</div>
</div>
<h5>{model.name}</h5>
<Button variant="danger" onClick={() => {
setImageModalShow(true)
setSelectedModel(model)}
}>
Ver
</Button>
</div>
);
}
return 0})}
</div>
<Image
show={imageModalShow}
onHide={() => setImageModalShow(false)}
model={selectedModel}
/>
</div>
<Footer />
</div>
)} else {
return (
<div className="loading">
<h1>Loading...</h1>
</div>
)}
}
export default Gallery;
Thanks for your time!
Models is a regular javascript function, not a functional component. So this is not a valid use of hooks, and will not work as expected. See docs on rules of hooks.
A functional component receives props and returns JSX or another React element.
Since it does not, it is basically restarting and calling your effect each time its called by the parent.
Looking at your edit, you should probably just remove the Models function and put the logic in the Gallery component.
The way I read your above component makes it seem like you've defined a custom hook for getting data from firebase.
So first off, I would rename it to useFbData and treat it as a custom hook, so that you can make use of the ESLint Plugin for Hooks and make sure you're following the rules of hooks.
The way you have this above, if it's a function within a component, your function will fire on every render, so the behaviour you are describing is what I would expect.
Depending on how expensive your request is/how often that component renders, this might be what you want, as you probably don't want to return stale data to your component. However, if you feel like the response from the DB should be cached and you have some logic to invalidate that data you could try something like this:
import { useEffect, useRef } from 'react';
const useFbData = invalidationFlag => {
const data = useRef(null);
useEffect(() => {
if (!data.current || invalidationFlag) {
firebase.database().ref('Data').on('value', (snapshot) => {
data.current = snapshot.val();
});
}
}, [invalidationFlag]);
return data.current;
};
export default useFbData;
This way, on the initial run and every time you changed the value of invalidationFlag, your effect inside the useFbData hook would run. Provided you keep track of invalidationFlag and set it as required, this could work out for you.
The reason I used a ref here instead of state, is so that the effect hook doesn't take the data in the dependency array (which would cause it to loop indefinitely if we used state).
This will persist the result of the db response between each call and prevent the call being made multiple times until you invalidate. Remember though, this will mean the data you're using is stale until you invalidate.
My particular use case of React is thus:
I wish to add a small React Component to a card that is an existing, fully-functional HTML element, per all the cards on the page. This React Component shall serve to implement a new feature on those cards : reverting changes.
The HTML (well, the MVCE version of it)
is something like this:
<div id="some-id" class="card float-sm-left menu-visual-card " onclick="(function(event) { console.log('I got clicked, and a modal will spawn' ) })(event)">
<div class=card-block>
<h5 class="card-title format-text">Some title</h5>
<!-- some business elements here -->
</div>
<!-- card footer -->
<div class=customized-indicator-react></div>
</div>
The React Component
in its tl;dr version is the following:
class CustomizedIndicatorComponent extends React.Component {
constructor(props) {
super(props)
// business logic
let active = this.props.active
this.state = {
active : active
}
}
toggleActive = () => {
this.setState({
...this.state,
active : !this.state.active
})
}
// setup
componentDidMount() {
// here's where I tried to add a jQuery onclick listener to stop propagation, only to have the React Component listener get stopped
}
// teardown
componentWillUnmount() {
console.log("CustomizedIndicatorComponent destroyed!")
}
// the UI logic
render() {
if (this.state.active) {
return (
<div>
<div
className="badge badge-sm badge-info float-sm-left customized"
style={{marginRight:"10px"}}
>Customized</div>
<div
onClick={(e) => {
e.stopPropagation()
this.toggleActive()
}}
title="Click to undo customizations">
<i className="fa fa-undo" aria-hidden="true"></i>
</div>
</div>
)
}
return <div />
}
}
What happens when you run this?
When I run this, it renders. However, when I click the widget to "de-activate" the element, the container's event-handler still fires!!
I know there is a slew of internet questions about this issue or something close to it, but none of the ones I could find seem to be about this exact use case.
Also, adding an event listener in componentDidMount doesn't work, as that prevents anything from firing!
Is there any way I can make this work without wasting developer-hours refactoring everything including the parent HTMLElements?
A "hacky" way you may consider is to get the parent's id from inside the React component and disable the click event from there.
If id could not be passed as a property to the React component, you can try using ReactDOM.findDOMNode(this).parentNode.getAttribute("id") to get it and then disable the event using:
document.getElementById(id).style.pointerEvents = 'none';