Call multiple functions in React, one inside and another outside a component? - javascript

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.

Related

Why does React rerender when the state is set to the same value the first time via an onClick event on DOM, but not react-native?

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>

javascript addEventListener in react js component not working

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 />}
...
}

react prop not updating at correct time when passed from parent component state

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.

Why does clicking on a tab that changes state and has nothing to do with ajax call creates that ajax call?

I have a simple layout.
There is a <StateSelector> in <Navbar> clicking on that executes a method.
The value of innerHTML of that button in <StateSelector> is passed as an argument to a function that was passed to it as a prop. And the method present in parent changes the activeOption State to All, Offline and online depending on the button clicked.
Now, There is one more child to this parent called <TwitchList>. This <TwitchList>'s render method contains an array of 8 users names and 8 calls are made to twitch.tv to get data for those channels.
Note that I have not linked <StateSelector> just yet. It has no interaction to $.ajax(); in <TwitchList> except the fact that <TwitchList> and <StateSelector> belong to same parent.
Why does clicking on a element inside <StateSelector> generating ajax calls?
And clicking on it one time generates 8 calls which is equal to number of users in usersList[].
I have tried searching for this issue and I have tried to work my way around for about 4 days now and I just don't understand why it is happenning.
var Navbar = React.createClass({
render: function () {
return (
<div className="navbar">
<h1 className="header">TWITCH STREAMERS</h1>
<StateSelector changeActiveOption={this.props.changeActiveOption}/>
</div>
);
}
});
var StateSelector = React.createClass({
changeOption: function (e) {
this.props.changeActiveOption(e.target.innerHTML.toLowerCase());
},
render: function () {
return (
<div className="selectorList">
<div className="selector" onClick={this.changeOption}>ALL</div>
<div className="selector" onClick={this.changeOption}>ONLINE</div>
<div className="selector" onClick={this.changeOption}>OFFLINE</div>
</div>
);
}
});
var TwitchList = React.createClass({
render: function () {
var userslist = ["ESL_SC2", "OgamingSC2", "cretetion", "freecodecamp", "storbeck", "habathcx", "RobotCaleb", "noobs2ninjas"];
var finalList = [];
function makeURL(user, type) {
return "https://api.twitch.tv/kraken/" + type + "/" + user;
}
userslist.forEach(function (user) {
$.ajax({
url: makeURL(user, "streams"),
dataType: 'jsonp',
success: function (data) {
function getID(data) {
if (data.stream) {
return data.stream._id;
} else {
return Math.random();
}
}
function getImage(data) {
if (!data.stream) {
return "https://dummyimage.com/50x50/ecf0e7/5c5457.jpg&text=0x3F";
}
else {
return data.stream.preview.medium;
}
}
console.log(data);
var id = getID(data);
var preview = getImage(data);
console.log(preview);
finalList.push(
<li className="twitchUser" key={id}>
<img src={preview} alt="preview"/>
</li>
)
},
fail: function (xhr, error, url) {
console.error(error + " " + xhr + " " + url);
}
});
});
return (
<div className= "twitchListWraper" >
<ul className="twitchList">
{finalList}
</ul>
</div>
)
}
});
var App = React.createClass({
getInitialState: function () {
return {
activeOption: "all"
};
},
changeActiveOption: function (option) {
this.setState({
activeOption: option
});
},
render: function () {
return (
<div className="app-root">
<Navbar changeActiveOption={this.changeActiveOption}/>
<TwitchList activeOption={this.state.activeOption}/>
</div>
);
}
});
ReactDOM.render(<App />, document.getElementById('root'));
JSBin: http://jsbin.com/sulihogegi/edit?html,css,js,console,output
Every time state changes on <App>, it re-renders. This includes re-rendering its children (on of which is <TwitchList>). Your ajax call is made in <TwitchList>'s render function, so every time state changes, it's going to be hitting that ajax code.
If you're wondering why the state's changing, it's because you have a function, changeActiveOption, that updates the <App>'s state being passed to <Navbar> which is then passes down to <StateSelector>.
The appropriate thing to do here is find a life cycle event in which to make the ajax call. I'd recommend componentWillMount and componentWillUpdate.
Take a look at lifecycle functions here.
you shouldn't write your ajax call to render function of TwitchList component instead you can write it to componentDidMount function

React component view does not get update

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);

Categories