I have a React component that manage multiple accordion in a list, but when i update a children, on React dev tools, it was showing the updated text but on the view/ui, it doesnt update. Please advice.
var AccordionComponent = React.createClass({
getInitialState: function() {
var self = this;
var accordions = this.props.children.map(function(accordion, i) {
return clone(accordion, {
onClick: self.handleClick,
key: i
});
});
return {
accordions: accordions
}
},
handleClick: function(i) {
var accordions = this.state.accordions;
accordions = accordions.map(function(accordion) {
if (!accordion.props.open && accordion.props.index === i) {
accordion.props.open = true;
} else {
accordion.props.open = false;
}
return accordion;
});
this.setState({
accordions: accordions
});
},
componentWillReceiveProps: function(nextProps) {
var accordions = this.state.accordions.map(function(accordion, i) {
var newProp = nextProps.children[i].props;
accordion.props = assign(accordion.props, {
title: newProp.title,
children: newProp.children
});
return accordion;
});
this.setState({
accordions: accordions
});
},
render: function() {
return (
<div>
{this.state.accordions}
</div>
);
}
Edit:
The component did trigger componentWillReceiveProps event, but it still never get updated.
Thanks
After days of trying to solve this, I have came out with a solution. componentWillReceiveProps event was trigger when the component's props changes, so there was no problem on that. The problem was that the childrens of the AccordionListComponent, AccordionComponent was not updating its values/props/state.
Current solution:
componentWillReceiveProps: function(nextProps) {
var accordions = this.state.accordions.map(function(accordion, i) {
// get current accordion key, and props value
// update new props with old
var newAc = clone(accordion, nextProps.children[i].props);
// update current accordion with new props
return React.addons.update(accordion, {
$set: newAc
});
});
this.setState({
accordions: accordions
});
}
I have tried to do only clone and reset the accordions, but apparently it did not update the component.
Thanks
I encountered a similar issue. So it may be relevant to report my solution here.
I had a graph that wasn't getting updated any time I hit just once the load data button (I could see visual changing after the second click).
The graph component was designed as a child of a parent component (connected to the Redux store) and data were passing therefore as a prop.
Issue: data were properly fetched from the store (in parent) but it seems the graph component (child) wasn't reacting to them.
Solution:
componentWillReceiveProps(nextProps) {
this.props = null;
this.props = nextProps;
// call any method here
}
Hope that might contribute in someway.
I usually handle this issue with below technique, its the solution if you are home-alone ;)
var _that = this;
this.setState({
accordions: new Array()
});
setTimeout(function(){
_that.setState({
accordions: accordions
});
},5);
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>
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 />}
...
}
I am experiencing a really annoying bug in react and believe it is to do with calling methods that are not in one of my components render method. I think I have provided far too much code, and it will be difficult for anyone to provide help but let me try and explain my problem.
I am making an API call that returns 25 items. When the user clicks next it should change the background and play the song related to the background. This works, but when the user gets to the end of the list I want to go back to the first song. The background image goes back, but the wrong song plays. Then when the user clicks again the problem corrects itself.
I believe the issue is due to this line in the nextSong method.
this.audio.src = this.state.songs[this.props.song + 1];
Please see complete code below. If necessary I can provide a github link, where the project can be cloned which may be easier to debug.
I know SO isn't for fixing other peoples code, but just in case I am missing some fundamental react knowledge I wanted to ask here.
var App = React.createClass({
getInitialState: function() {
return {
data: [],
song: 0
};
},
nextSong: function() {
if(this.state.song === this.state.data.length) {
this.setState({song : 0});
}else{
this.setState({song : this.state.song += 1});
}
},
previousSong: function() {
if(this.state.song === 0) {
this.setState({song : this.state.data.length});
}else{
this.setState({song : this.state.song -= 1});
}
},
getData: function(){
$.ajax({
url: '...',
method: 'GET',
success: function(response){
this.setState({data: response.items});
}.bind(this)
})
},
render: function() {
return(
<div>
<BackgroundImage src={this.state.data} image={this.state.song} />
<Button src={this.state.data} song={this.state.song} nextSong={this.nextSong} previousSong={this.previousSong} />
</div>
)
}
});
var BackgroundImage = React.createClass({
render: function() {
var images = this.props.src.map(function(photo, i){
return(photo.track.album.images[1].url)
})
var divStyle = {
background: 'url(' + images[this.props.image] + ')'
}
return(<div style={divStyle}></div>)
}
});
var Button = React.createClass({
getInitialState: function(){
return{
songs:[],
playing: false
}
},
audio: new Audio,
playSong: function(){
this.setState({playing: true});
this.audio.src = this.state.songs[this.props.song];
this.audio.play();
},
pauseSong: function() {
this.setState({playing: false});
this.audio.pause();
},
nextSong: function() {
this.audio.src = this.state.songs[this.props.song + 1];
this.audio.play();
},
onClickNext: function(){
this.setState({playing: true});
this.props.nextSong();
this.nextSong()
},
previousSong: function() {
this.setState({playing: true});
this.audio.src = this.state.songs[this.props.song - 1];
this.audio.play();
},
onClickPrevious: function(){
this.props.previousSong();
this.previousSong()
},
render: function() {
var self = this;
var songs = this.props.src.map(function(song, i){
self.state.songs.push(song.track.preview_url)
});
return (
<div className="button">
<div className="next" onClick={this.onClickNext}>
NEXT
</div>
<div className="pause">
<p onClick={this.state.playing ? this.pauseSong : this.playSong}></p>
</div>
<div className="prev" onClick={this.onClickPrevious}>
PREVIOUS
</div>
</div>
)
}
});
First of all, you should not do this:
this.setState({song : this.state.song += 1});
Because that will first set this.state.song and then setState, and this could make some conflict as you're updating state without alerting react. I'm not sure if react handles that correctly. Instead, simply do:
this.setState({song : this.state.song + 1});
(Same with -=)
Back to the question: you should keep track of the current song only in one place. In this case, you should keep it in the App component's state, and button component shouldn't.
So methods like nextSong have no place in your button component.
Instead, you update the song in your App component (like you're doing in its nextSong method) and pass the current song directly to your button component via props.
Then, you can detect whether the current song changes with lifecycle method componentWillReceiveProps like this:
componentWillReceiveProps: function(nextProps) {
this.playSong(nextProps.song);
}
So, when you change state in App, it'll update and pass new props to Button, then Button will trigger componentWillReceiveProps with the new song and do its stuff.
You could try using componentWillUpdate(nextProps, nextState) lifecycle method to get the props as they're available. Like so:
componentWillUpdate(nextProps, nextState) {
if (nextProps.song !=== this.props.song) {
this.audio.src = this.state.songs[nextProps.song];
this.audio.play();
}
}
Also, you wouldn't need to have the this.audio.src=... and this.audio.play() portion in the previousSong() and nextSong() methods.
I took a look to your code at Github and I found that changing those methods in <App /> component to look like:
nextSong: function() {
console.log(this.state.song)
if(this.state.song === this.state.data.length - 1) {
this.setState({song : 0});
}else{
this.setState({song : this.state.song + 1});
}
},
previousSong: function() {
if(this.state.song === 0) {
this.setState({song : this.state.data.length - 1});
}else{
this.setState({song : this.state.song - 1});
}
},
Note that you have to make the last index be equal to data.length - 1 as data.length will be undefined.
Also, I've changed those methods in <Button /> component as well:
onClickNext: function(){
this.setState({playing: true});
this.props.nextSong();
},
onClickPrevious: function(){
this.props.previousSong();
},
And now, it seems to be working here for me.
The reason this happens is that you update the track index correctly in your App component, but not in your Button component. When you reach the last song, and call this.props.nextSong() from Button, App will set the index to 0. But after that, you also call this.nextSong() (from Button), which will play audio from the track at index this.props.song + 1.
This is probably where you've misunderstood things a bit. Here, this.props.song will still be the same as it was in onClickNext(), so it will actually try to play a song that doesn't exist (in your example it will try to play the song at index 25, but there's only songs at 0-24).
An easy solution would be to use the same logic in your Button, i.e. check when you've reached the end, and then play the song at index 0. But now you're keeping track of the same state twice, which really isn't a good idea.
A better solution would be to do as suggested in this answer, or just let App play the audio.
I have a trigger that is supposed to change a state of its child component. The results of both render statements are inconsequential. My only concern here is that I am unsure how to use the trigger trigger to call the leftbehind function without putting leftbehind inside its parent render Another.
My code is below. The goal is to have leftbehind run without having to put it inside the render.
var Another = React.createClass({
leftbehind: function() {
if (this.props.status === "dare") {
alert('Winning!');
}
},
render: function() {
if (this.props.status === "truth") {
return (<p>Yes</p>);
} else {
return (<p>Nope</p>);
}
}
});
var App = React.createClass({
getInitialState:function() {
return {deesfault: "truth"};
},
trigger: function() {
this.setState({deesfault: "dare"});
},
render: function() {
return (
<div>
<p onClick={this.trigger}>{this.state.deesfault}</p>
<Another status={this.state.deesfault}/>
</div>
);
}
});
The reason I do not want to place leftbehind inside the render, is because it is technically supposed to take the place of an API call. And I do not want that to be called inside the render function.
Your implementation executes leftbehind each time <Another> is rendering with its status prop being true. That said, once status is flipped to true, leftbehind will be executed over and over in every following rendering until it is flipped back to false. This will seriously cause problems.
Since the intention is to trigger leftbehind with a click event, I would restructure the components in different ways.
Move leftbehind into the parent component and have it executed along with the click event. If <Another> needs the results, passed them on through props.
var Another = React.createClass({
render() {
return <div>{this.props.params}</div>;
}
});
var App = React.createClass({
getInitialState() {
return {apiRes: null};
},
onClick() {
const res = someAPICall();
this.setState({apiRes: res});
},
render() {
return (
<div>
<p onClick={this.onClick}>Fire</p>
<Another params={this.state.apiRes} />
</div>
);
}
});
Or, move the <p> element into <Another> along with the click event.
var Another = React.createClass({
getInitialState() {
return {apiRes: null};
},
onClick() {
var res = someAPICall();
this.setState({apiRes: res});
},
render() {
return (
<div>
<p onClick={this.onClick}>Fire</p>
<div>{this.state.apiRes}</div>
</div>
);
}
});
var App = function() { return <Another />; }
In the latter, the key logic is handled in the inner component. The outer one is just a container. In the former one, the outer component handles the logic and pass on the results if any. It depends on how the components relate with the API call to decide which suits better. Most importantly, in both cases the API will not execute unless the click event is triggered.
So i'm relatively new to React.
I've seen quite a few questions about this but all the solutions have the onClick event attached and executed from within the React component.
I'm wondering, can I interact with my react component the same way I normally would? So my below is a react component
var tableMain = React.createClass({
render: function render() {
console.log(tableModel, "summon the JSON data from tableModel!!");
var thead = React.DOM.thead({},
React.DOM.tr({},
tableModel.map(function (col) {
return React.DOM.th({}, col);
})));
var tbody = tableModel.rows.map(function (row) {
return React.DOM.tr({},
tableModel.cols.map(function (col) {
return React.DOM.td({}, row[col] || "");
}));
});
return React.DOM.table({}, [thead, tbody]);
}
});
Then in my js file, have an onlick event?