I'm trying to create simple accordion in React. If I open/refresh whole page, onclick event is not triggered when I click on buttons. It works properly only after I change something in code and component rerenders. No bugs in console and honestly I have no idea what is happening here. Thanks in advance
import { useEffect } from "react"
function FAQ () {
const accordionBtn = document.querySelectorAll('.accordionTitle')
const allTexts = document.querySelectorAll('.text')
useEffect(() => {
accordionBtn.forEach(function (el) {
el?.addEventListener('click', toggleAccordion)
})
}, [])
function toggleAccordion (el: Event) {
const targetText = (el?.currentTarget as Element).nextElementSibling?.classList
const target = (el?.currentTarget as Element).classList
if (!(targetText?.contains('show'))) {
accordionBtn.forEach(function (el) {
el.classList.remove('accordionTitleActive')
allTexts.forEach(function (el) {
el.classList.remove('show')
})
})
targetText?.add('show')
target?.add('accordionTitleActive')
}
}
return (
<div>
<ul className='accordion'>
<li>
<h2 className='accordionTitle'>Title1 </h2>
<div className='text show'>
Content1
</div>
</li>
<li>
<h2 className='accordionTitle'>Title2 </h2>
<div className='text'>
Content2
</div>
</li>
<li>
<h2 className='accordionTitle'>Title3 </h2>
<div className='text'>
Content3
</div>
</li>
</ul>
</div>
)
}
export default FAQ
When you retrieve the DOM elements at that time, those elements are not even in DOM. If you move the querySelectorAll calls to useEffect it should solve your problem. Remember that useEffect callback is called after the component has been painted on the screen.
But as Andy mentioned in comment above it is strongly not recommended to access DOM directly. Even if you have to(which I don't think is required in your scenario) there is useRef
Here's how I might approach it. It uses state to maintain the number of the accordian, and then you can just adjust the class of the child elements accordingly.
const { useState } = React;
function Example() {
// Use state to identify the accordian you want open
const [ show, setShow ] = useState(0);
// `handleClick` grabs the id from the accordian
// and then sets the state
function handleClick(e) {
const { id } = e.target.parentNode.dataset;
setShow(Number(id));
}
// Now, for each element check to see if the id matches
// the state and show that text
return (
<div onClick={handleClick}>
<div data-id="1">
<h3>Accordian one</h3>
<p className={show === 1 && "show"}>Accordian one text</p>
</div>
<div data-id="2">
<h3>Accordian two</h3>
<p className={show === 2 && "show"}>Accordian two text</p>
</div>
<div data-id="3">
<h3>Accordian three</h3>
<p className={show === 3 && "show"}>Accordian three text</p>
</div>
</div>
);
};
// Render it
ReactDOM.render(
<Example />,
document.getElementById("react")
);
p { display: none; }
h3:hover { cursor: pointer; }
.show { display: block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I would suggest - Refactor your code to use class-based components. Not that the existing code is ugly, but you should use best practices when it comes to React and rendering html smoothly. Moreover, by doing so, you can also use React Component Lifecycle methods - which will be very handy!
A good article on class based components : Link