I am trying to add an onClick event handler to objects in an array where the class of a clicked object is changed, but instead of only changing one element's class, it changes the classes of all the elements.
How can I get the function to work on only one section element at a time?
class Tiles extends React.Component {
constructor(props) {
super(props);
this.state = {
clicked: false,
content : []
};
this.onClicked = this.onClicked.bind(this);
componentDidMount() {
let url = '';
let request = new Request(url, {
method: 'GET',
headers: new Headers({
'Content-Type': 'application/json'
})
});
fetch(request)
.then(response => response.json())
.then(data => {
this.setState({
content : data
})
} );
}
onClicked() {
this.setState({
clicked: !this.state.clicked
});
}
render() {
let tileClass = 'tile-content';
if (this.state.clicked) {
tileClass = tileClass + ' active'
}
return (
<div className = 'main-content'>
{this.state.pages.map((item) =>
<section key = {item.id} className = {tileClass} onClick = {this.onClicked}>
<h4>{item.description}</h4>
</section>)}
<br />
</div>
)
}
class App extends React.Component {
render() {
return (
<div>
<Tiles />
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('content-app'))
You have onClicked() define in your 'main-content' class. So that's where it fires.
constructor(props) {
// super, etc., code
this.onClicked = this.onClicked.bind(this); // Remove this line.
}
Remove that part.
You can keep the onClicked() function where it is. Your call in render() is incorrect, though: onClick = {this.onClicked}>. That accesses the onClicked ATTRIBUTE, not the onClicked FUNCTION, it should be this.onClicked().
Let me cleanup your call in render() a little bit:
render() {
let tileClass = 'tile-content';
return (
<div className = 'main-content'>
// some stuff
<section
key={item.id}
className={tileClass}
onClick={() => this.onClicked()} // Don't call bind here.
>
<h4>{item.description}</h4>
</section>
// some other stuff
</div>
)
}
It is happening for you, because you are assigning active class to all sections once user clicked on one of them. You need somehow to remember where user clicked. So I suggest you to use array, where you will store indexes of all clicked sections. In this case your state.clicked is an array now.
onClicked(number) {
let clicked = Object.assign([], this.state.clicked);
let index = clicked.indexOf(number);
if(index !== -1) clicked.splice(index, 1);
else clicked.push(number)
this.setState({
clicked: clicked
});
}
render() {
let tileClass = 'tile-content';
return (
<div className = 'main-content'>
{this.state.pages.map((item, i) => {
let tileClass = 'tile-content';
if(this.state.clicked.includes(i)) tile-content += ' active';
return (
<section key = {item.id} className = {tileClass} onClick = {this.onClicked.bind(this, i)}>
<h4>{item.description}</h4>
</section>
)
})}
<br />
</div>
)
}
StackOverflow does a particularly poor job of code in comments, so here's the implementation of onClicked from #Taras Danylyuk using the callback version of setState to avoid timing issues:
onClicked(number) {
this.setState((oldState) => {
let clicked = Object.assign([], this.state.clicked);
let index = clicked.indexOf(number);
if(index !== -1) {
clicked.splice(index, 1);
} else {
clicked.push(number);
}
return { clicked };
});
}
The reason you need this is because you are modifying your new state based on the old state. React doesn't guarantee your state is synchronously updated, and so you need to use a callback function to make that guarantee.
state.pages need to keep track of the individual click states, rather than an instance-wide clicked state
your onClick handler should accept an index, clone state.pages and splice your new page state where the outdated one used to be
you can also add data-index to your element, then check onClick (e) { e.currentTarget.dataset.index } to know which page needs to toggle clickstate
Related
I have what I thought would be a simple test to prove state changes, I have another test which does the change by timer and it worked correctly (at least I am assuming so) but this one is trigged by a click event and it's failing my rerender check.
it("should not rerender when setting state to the same value via click", async () => {
const callback = jest.fn();
function MyComponent() {
const [foo, setFoo] = useState("bir");
callback();
return (<div data-testid="test" onClick={() => setFoo("bar")}>{foo}</div>);
}
const { getByTestId } = render(<MyComponent />)
const testElement = getByTestId("test");
expect(testElement.textContent).toEqual("bir");
expect(callback).toBeCalledTimes(1);
act(() => { fireEvent.click(testElement); });
expect(testElement.textContent).toEqual("bar");
expect(callback).toBeCalledTimes(2);
act(() => { fireEvent.click(testElement); });
expect(testElement.textContent).toEqual("bar");
expect(callback).toBeCalledTimes(2); // gets 3 here
})
I tried to do the same using codesandbox https://codesandbox.io/s/rerender-on-first-two-clicks-700c0
What I had discovered looking at the logs is it re-renders on the first two clicks, but my expectation was it on re-renders on the first click as the value is the same.
I also did something similar on React native via a snack and it works correcty. Only one re-render. So it may be something specifically onClick on React-DOM #22940
Implement shouldComponentUpdate to render only when state or
properties change.
Here's an example that uses shouldComponentUpdate, which works
only for this simple use case and demonstration purposes. When this
is used, the component no longer re-renders itself on each click, and
is rendered when first displayed, and after it's been clicked once.
var TimeInChild = React.createClass({
render: function() {
var t = new Date().getTime();
return (
<p>Time in child:{t}</p>
);
}
});
var Main = React.createClass({
onTest: function() {
this.setState({'test':'me'});
},
shouldComponentUpdate: function(nextProps, nextState) {
if (this.state == null)
return true;
if (this.state.test == nextState.test)
return false;
return true;
},
render: function() {
var currentTime = new Date().getTime();
return (
<div onClick={this.onTest}>
<p>Time in main:{currentTime}</p>
<p>Click me to update time</p>
<TimeInChild/>
</div>
);
}
});
ReactDOM.render(<Main/>, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react-dom.min.js"></script>
I have some code that unpacks an array and returns an array of JSX elements. This works fine, however, when one element is clicked, the "selectedEl" css class is removed from all other elements.
I'm pretty sure I've just made some stupid mistake but I can't seem to figure it out. Thanks
Code that unpacks the array and assigns onClick method:
function UnpackReccArray() {
const renderArray = []
for (let renderEl = 0; renderEl < self.state.capMethod; renderEl++) {
renderArray.push(
<span className="topicElement" onClick={self.pushToChosen.bind(this, self.state.reccDataQuery[renderEl].topicID, renderEl + "topicEl")} id={renderEl + "topicEl"}>
<p className="fontLibre">{self.state.reccDataQuery[renderEl].displayName}</p>
</span>
)
if (renderEl + 1 === self.state.capMethod) {
return (
<self.ResultRender title="Popular Subjects" renderContent={renderArray} />
)
}
}
}
Code that handles onClick function
pushToChosen = (id, elID) => {
const self = this
const localChoseArray = this.state.subjectChosenArray
const index = localChoseArray.indexOf(id.toString())
if (index > -1) {
localChoseArray.splice(index, 1);
self.setState({
subjectChosenArray: localChoseArray
}, () => {
document.getElementById(elID).classList.remove("selectedEl")
})
} else {
self.setState({
subjectChosenArray: [...this.state.subjectChosenArray, id.toString()]
}, () => {
document.getElementById(elID).classList.add("selectedEl")
})
}
document.getElementById(elID).classList.toggle("selectedEl")
}
GIF of the code in action
Thanks!
i didn't fully understand the code but it seems that the toggle is what causing the issue since the if and else handles all possible cases , toggle is probably going to do the opposite of the if else bloc .
I would like to implement something like this using React hooks:
const header = document.querySelector(".nav-header");
function stickyHeader() {
if (window.pageYOffset > 600) {
header.classList.remove("header");
} else {
header.classList.add("header");
}
}
window.addEventListener("scroll", () => {
stickyHeader();
});
Above, is how I would have manipulated the DOM in vanilla js. I would like to do the same for a component in react.
Possibly try a getClassName method which returns a joined array of classes on every render
For a functional component
const getClassName = () => {
let classes = ["nav"];
if (window.pageYOffset > 600) {
classes.push("header");
}
//more conditions if required
return classes.join(" ");
//returns "nav header" || "nav"
}
and then in your component return method
return (
<div className={getClassName()}><div>
)
className is an element attribute. Therefore you need to set it in your render component function like below:
...
render() {
let className = 'menu';
if (this.props.isActive) {
className += ' menu-active';
}
return <span className={className}>Menu</span>
}
here is an example on how to handle scroll events with React
How do I get this functionality using React?
function myFunction() {
var node = document.createElement("div");
var textnode = document.createTextNode("Button was clicked.");
node.appendChild(textnode);
document.getElementById("root").appendChild(node);
}
in your render function write the div you want and make a state
then change a state to true to the div to be shown
somthing like this
constructor(props) {
super(props)
this.state = {
showDiv: false
}
}
render (){
return (
{
this.state.showDiv ? your div : <React.Fragment />
}
)
}
changeDivDisplay(){
this.setState({showDiv:true})
}
also i do recommend you to read about the state and the refs of react
At first run the program is working correctly.
But after clicking on the sum or minus button the function will not run.
componentDidMount() {
if(CONST.INTRO) {
this.showIntro(); // show popup with next and prev btn
let plus = document.querySelector('.introStep-plus');
let minus = document.querySelector('.introStep-minus');
if (plus || minus) {
plus.addEventListener('click', () => {
let next = document.querySelector(CONST.INTRO[this.state.introStep].element);
if (next) {
next.parentNode.removeChild(next);
}
this.setState({
introStep: this.state.introStep + 1
});
this.showIntro();
});
}
}
As referenced in React documentation: refs and the dom the proper way to reference DOM elements is by using react.creatRef() method, and even with this the documentation suggest to not over use it:
Avoid using refs for anything that can be done declaratively.
I suggest that you manage your .introStep-plus and introStep-minus DOM element in your react components render method, with conditional rendering, and maintain a state to show and hide .introStep-plus DOM element instead of removing it inside native javascript addEventListener click
Here is a suggested modification, it might not work directly since you didn't provide more context and how you implemented the whole component.
constructor() {
...
this.state = {
...
showNext: 1,
...
}
...
this.handlePlusClick = this.handlePlusClick.bind(this);
}
componentDidMount() {
if(CONST.INTRO) {
this.showIntro(); // show popup with next and prev btn
}
}
handlePlusClick(e) {
this.setState({
introStep: this.state.introStep + 1,
showNext: 0,
});
this.showIntro();
});
render() {
...
<div className="introStep-plus" onClick={this.handlePlusClick}></div>
<div className="introStep-minus"></div>
{(this.stat.showNext) ? (<div> NEXT </div>) : <React.fragment />}
...
}