Call a callback each time state changes in React [duplicate] - javascript

This question already has answers here:
Why is setState in reactjs Async instead of Sync?
(8 answers)
Closed 5 years ago.
I have an app like:
Main.js-
import React, { Component } from 'react';
import _ from 'underscore';
import { pick_attributes } from '../utils/general';
import ApplicationsButtons from '../components/ApplicationsButtons';
import Roles from '../components/Roles';
let applications_url = 'http://127.0.0.1:8889/api/applications'
export default class Main extends Component {
constructor(props) {
super(props);
this.state = {
applications: [],
selected_app_id: 1,
roles: []
};
this.updateSelectedApp = this.updateSelectedApp.bind(this);
this.updateApplicationData = this.updateApplicationData.bind(this);
this.loadAppData = this.loadAppData.bind(this);
this.getSelectedApplicationData = this.getSelectedApplicationData.bind(this);
this.setRoles = this.setRoles.bind(this);
}
componentDidMount() {
this.loadAppData();
}
// componentDidUpdate() {
// this.updateApplicationData();
// }
updateApplicationData() {
this.setRoles();
}
loadAppData() {
let self = this;
$.ajax({
url: applications_url,
method: 'GET',
success: function(data) {
let objects = data.objects;
self.setState({applications_data: objects});
let apps_data = pick_attributes(objects, 'name', 'id');
self.setState({applications: apps_data});
self.updateApplicationData();
}
});
}
getSelectedApplicationData() {
let selected_app_id = this.state.selected_app_id;
let objects = this.state.applications_data;
for (let i = 0; i < objects.length; i++) {
let object = objects[i];
if (object.id == selected_app_id) {
return object
}
}
}
setRoles() {
let selected_app_id = this.state.selected_app_id;
let selected_app_object = this.getSelectedApplicationData();
let roles_data = selected_app_object.role_list;
let roles = pick_attributes(roles_data, 'name', 'id');
this.setState({roles});
}
updateSelectedApp(id) {
this.setState({selected_app_id: id});
}
render() {
return (
<div>
{this.state.selected_app_id}
<ApplicationsButtons
apps={this.state.applications}
clickHandler={this.updateSelectedApp}/>
<Roles roles={this.state.roles} />
</div>
);
}
}
ApplicationsButtons.js-
import React, { Component } from 'react';
export default class ApplicationsButtons extends Component {
render() {
var buttons = null;
let apps = this.props.apps;
let clickHandler = this.props.clickHandler;
if (apps.length > 0) {
buttons = apps.map(function(app) {
return (
<button
onClick={() => clickHandler(app.id)}
key={app.id}>
{app.name} - {app.id}
</button>
);
});
}
return (
<div>
{buttons}
</div>
);
}
}
Roles.js-
import React, { Component } from 'react';
export default class Roles extends Component {
render() {
var roles_li_elements = null;
let roles = this.props.roles;
console.log(roles);
if (roles.length > 0) {
roles_li_elements = roles.map(function(role) {
console.log(role);
return (
<li key={role.id}>
{role.name}
</li>
);
});
}
return (
<div>
<h4>Roles:</h4>
<ul>
{roles_li_elements}
</ul>
</div>
);
}
}
I want the Roles to update when the user clicks a button that picks a new app. Right now, clicking the buttons does update state.selected_app_id, but I need setRoles() to be called each time selected_app_id changes. I tried throwing it in the onClick:
updateSelectedApp(id) {
this.setState({selected_app_id: id});
this.setRoles();
}
for some reason that only changed the roles after clicking each button twice.
componentDidUpdate() {
this.updateApplicationData();
}
causes state to update forever in an infinite loop. You aren't supposed to update state inside componentWillUpdate.

updateSelectedApp(id) {
this.setState({selected_app_id: id}, () => {
this.setRoles();
});
}

Related

How to call function from function array (React js)

I Created array that contains functions and now I am trying to call a function from that array, I tried this code but it's not working and I have no idea how can I do it becuase I am pretty new at react js. Can someone help me with this?
here is my code
import React, { Component } from "react";
import "../App.css";
export default class Chart extends Component {
constructor(props) {
super(props);
this.state = {
stockChartXValues: "",
stockChartYValues: "",
type: props.data,
};
}
getBTC = () => {
// .....
};
getETH = () => {
// .....
};
getADA = () => {
// .....
};
componentDidMount() {
// here I am trying to run a function according to the "type" variable
var options = ["BTC", "ETH", "ADA"];
var functions = [this.getBTC, this.getETH, this.getADA];
var func = functions.indexOf(options.indexOf(this.state.type));
func.call();
}
render() {
return (
<div>
<h1>Hello world</h1>
</div>
);
}
}
you need to get function with the index you found;
var func = functions.indexOf(options.indexOf(this.state.type));// this returns index not the actual func
functions[func] && functions[func]()
My Approach would be like;
getBTC = () => {
// .....
};
getETH = () => {
// .....
};
getADA = () => {
// .....
};
getCoin = (type) => {
switch(type) {
case "BTC": this.getBTC()
return
case "ADA":...
...
...
}
componentDidMount() {
this.getCoin(this.state.type)
}

ReactJS - Buttons not Re-Rendering on State Change?

React Newbie here,
import React, { Component } from "react";
class AudioList extends Component {
constructor(props) {
super(props);
this.audios = [];
this.buttonText = [];
for (let i = 0; i < this.props.songs.length; i++) {
this.audios.push(new Audio(this.props.songs[i].song_url));
this.buttonText.push(String(i));
}
this.state = {
songs: "",
buttonText: this.buttonText
};
}
componentWillMount() {
const songs = [];
for (let i = 0; i < this.props.songs.length; i++) {
this.audios[i].addEventListener("play", () => {
let stateArray = [...this.state.buttonText];
let stateArrayElement = { ...stateArray[i] };
stateArrayElement = "playing";
stateArray[i] = stateArrayElement;
console.log(stateArray);
this.setState({ buttonText: stateArray });
console.log(this.state.buttonText[i]);
});
songs.push(
<div className="song-preview">
<button
className="preview"
onClick={() => this.toggle(this.audios[i])}
>
{this.state.buttonText[i]}
</button>
</div>
);
}
this.setState({
songs: songs
});
}
componentWillUnmount() {
for (let i = 0; i < this.props.songs.length; i++) {
this.audios[i].pause();
}
}
getCurrentAudio() {
return this.audios.find(audio => false === audio.paused);
}
toggle(nextAudio) {
const currentAudio = this.getCurrentAudio();
if (currentAudio && currentAudio !== nextAudio) {
currentAudio.pause();
nextAudio.play();
}
nextAudio.paused ? nextAudio.play() : nextAudio.pause();
}
render() {
if (this.state.songs) {
return <div className="song-list">{this.state.songs}</div>;
} else {
return <div className="song-list"></div>;
}
}
}
export default AudioList;
I am using this code from a previous solution that I found on Stackoverflow (https://stackoverflow.com/a/50595639). I was able to implement this solution to solve my own challenge of needing to have multiple audio sources with one audio player and multiple buttons. However, I am now faced with a new challenge - I want a specific button's text to change when an event is fired up.
I came up with this implementation where the button text is based on an array in the state called buttonText. The buttons are rendered correctly on startup, but when the event listener picks up the event and changes the state, the text in the button is not re-rendering or changing, even though it is based on an element in an array in the state that is changing.
Does anyone have any suggestions about why it may be failing to re-render?
EDIT: Changing an individual array element in the state is based on React: how to update state.item[1] in state using setState?
I have restructured your code a bit (but it's untested it's tested now):
const songs = [
{
title: "small airplane long flyby - Mike_Koenig",
song_url: "http://soundbible.com/mp3/small_airplane_long_flyby-Mike_Koenig-806755389.mp3"
},
{
title: "Female Counts To Ten",
song_url: "http://soundbible.com/mp3/Female%20Counts%20To%20Ten-SoundBible.com-1947090250.mp3"
},
];
class AudioList extends React.Component {
audios = new Map();
state = {
audio: null,
song: null
};
componentDidUpdate() {
// stop playing if the song has been removed from props.songs
if (this.state.song && !this.props.songs.some(song => song.song_url === this.state.song.song_url)) {
this.toggle(this.state.audio, this.state.song);
}
}
componentWillUnmount() {
if (this.state.audio) {
this.state.audio.pause();
}
this.audios.clear();
}
toggle(audio, song) {
this.setState(state => {
if (audio !== state.audio) {
if (state.audio) {
state.audio.pause();
}
audio.play();
return { audio, song };
}
audio.pause();
return { audio: null, song: null };
});
}
getAudio(song) {
let audio = this.audios.get(song.song_url);
if (!audio) {
this.audios.set(song.song_url, audio = new Audio(song.song_url));
}
return audio;
}
render() {
return <div className="song-list">{
this.props.songs.map((song, i) => {
const audio = this.getAudio(song);
const playing = audio === this.state.audio;
return <div className="song-preview">
<button
className="preview"
onClick={this.toggle.bind(this, audio, song)}
>
{playing ? "playing" : (song.title || i)}
</button>
</div>
})
}</div>;
}
}
ReactDOM.render(<AudioList songs={songs} />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Edit: added a title to the song-objects and display them on the buttons
I simply moved all of the code from componentWillMount() to render(). I also removed 'songs' as a state variable and set it to a variable that exists only in render as songs is simply just a set of divs.
import React, { Component } from "react";
const audio1 =
"http://soundbible.com/mp3/small_airplane_long_flyby-Mike_Koenig-806755389.mp3";
const audio2 =
"http://soundbible.com/mp3/Female%20Counts%20To%20Ten-SoundBible.com-1947090250.mp3";
class AudioList extends Component {
constructor(props) {
super(props);
this.audios = [];
this.buttonText = [];
for (let i = 0; i < this.props.songs.length; i++) {
this.audios.push(new Audio(this.props.songs[i]));
this.buttonText.push(String(i));
}
this.state = {
buttonText: this.buttonText
};
}
componentWillUnmount() {
for (let i = 0; i < this.props.songs.length; i++) {
this.audios[i].pause();
}
}
getCurrentAudio() {
return this.audios.find(audio => false === audio.paused);
}
toggle(nextAudio) {
const currentAudio = this.getCurrentAudio();
if (currentAudio && currentAudio !== nextAudio) {
currentAudio.pause();
nextAudio.play();
}
nextAudio.paused ? nextAudio.play() : nextAudio.pause();
}
render() {
const songs = [];
for (let i = 0; i < this.props.songs.length; i++) {
this.audios[i].addEventListener("play", () => {
console.log("playing");
let stateArray = [...this.state.buttonText];
let stateArrayElement = { ...stateArray[i] };
stateArrayElement = "playing";
stateArray[i] = stateArrayElement;
console.log(stateArray);
this.setState({ buttonText: stateArray });
console.log(this.state.buttonText);
});
songs.push(
<div className="song-preview">
<button
className="preview"
onClick={() => this.toggle(this.audios[i])}
>
{this.state.buttonText[i]}
</button>
</div>
);
}
return (
<div>{songs}</div>
)
}
}
export default () => <AudioList songs={[audio1, audio2]} />;
The code now runs as expected.

Reactjs: Pressing button to randomize an array

Basically, I am trying to make a card program that would pick five cards out of 52 in random. These cards must not repeat. I have already figured out the randomizer through traditional javascript. However, I am using ReactJs to make a button which if pressed, would create a new set of five cards.
class Reset extends React.Component {
constructor(props) {
super(props);
this.state = {...};
}
handleClick() {...}
render() {
return <button onClick={this.handleClick}>{...}</button>;
}
}
const cards = [
"A♥",
"A♠",
"A♦",
"A♣",
"2♣",
"3♣",
"4♣",
"5♣",
"6♣",
"7♣",
"8♣",
"9♣",
"10♣",
"K♣",
"Q♣",
"J♣",
"2♦",
"3♦",
"4♦",
"5♦",
"6♦",
"7♦",
"8♦",
"9♦",
"10♦",
"K♦",
"Q♦",
"J♦",
"2♥",
"3♥",
"4♥",
"5♥",
"6♥",
"7♥",
"8♥",
"9♥",
"10♥",
"K♥",
"Q♥",
"J♥",
"2♠",
"3♠",
"4♠",
"5♠",
"6♠",
"7♠",
"8♠",
"9♠",
"10♠",
"K♠",
"Q♠",
"J♠"
];
var hand = [];
function in_array(array, el) {
for (var i = 0; i < array.length; i++) if (array[i] == el) return true;
return false;
}
function get_rand(array) {
var rand = array[Math.floor(Math.random() * array.length)];
if (!in_array(hand, rand)) {
hand.push(rand);
return rand;
}
return get_rand(array);
}
for (var i = 0; i < 5; i++) {
document.write(get_rand(cards));
}
ReactDOM.render(<Reset />, document.getElementById("root"));
Basically, what would I have to fill in the parts with "..." in order for the code to rerandomize the pack.
Try something like this, I'm preserving alot of the code you already wrote. You really just have to move that logic into the handler.
Here's the sandbox as well: https://codesandbox.io/s/yv93w19pkz
import React from "react";
import ReactDOM from "react-dom";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
cards: ["A♥", "A♠", "A♦", "A♣", "2♣", "3♣", "4♣", "5♣", "6♣", "7♣", "8♣", "9♣", "10♣", "K♣", "Q♣", "J♣", "2♦", "3♦", "4♦", "5♦", "6♦", "7♦", "8♦", "9♦", "10♦", "K♦", "Q♦", "J♦", "2♥", "3♥", "4♥", "5♥", "6♥", "7♥", "8♥", "9♥", "10♥", "K♥", "Q♥", "J♥", "2♠", "3♠", "4♠", "5♠", "6♠", "7♠", "8♠", "9♠", "10♠", "K♠", "Q♠", "J♠"],
hand: []
}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
const cards = this.state.cards
const newHand = []
function get_rand(array) {
var rand = array[Math.floor(Math.random() * array.length)];
if (!newHand.includes(rand)) {
newHand.push(rand);
} else {
get_rand(cards);
}
}
for (var i = 0; i < 5; i++) {
get_rand(cards);
}
this.setState({
hand: newHand
})
}
render() {
const { hand } = this.state
return (
<div>
{ hand ? (hand.map((card) => {
return <p>{card}</p>
})) : (
null
)}
<button onClick={this.handleClick}>Randomize
</button>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
Try to declare the cards in the state and update when you click on it

React TypeError: this.fetchPosts is not a function

After setup a simple pagination and try to fetch the next page, the log show this error:
React TypeError: this.fetchPosts is not a function
handleChangePage(page) {
this.fetchPosts(page);
}
please, someone can enlightenment why can just pass a function to action triggers?
so what is the purpose of action from the component?
the pagination component:
import React, { Component } from "react";
class Pagination extends Component {
paginationElement(number) {
return (
<li key={'page' + number}
className={number == this.props.page ? 'active' : ''}>
<a onClick={this.props.handleChangePage.bind(this, number)}>{number}</a>
</li>
)
}
render() {
var self = this;
var page = this.props.page;
var last_page = this.props.pages;
var page_links = [];
var max_elements = 2;
var pages = [1];
for (var i = page - max_elements; i <= page + max_elements; i++) {
if (!pages.includes(i))
pages.push(i);
}
if (!pages.includes(last_page))
pages.push(last_page);
pages.forEach(function(i) {
if (i > 0 && i <= last_page)
page_links.push(self.paginationElement(i));
});
return(
<div className="text-center">
<ul className="pagination">
{page_links}
</ul>
</div>
);
}
}
export default Pagination
the trigger
import React, { Component } from "react";
import Pagination from "../components/pagination";
import axios from 'axios';
class Posts extends Component {
constructor(props) {
super(props);
this.state = {
posts: [],
page: 1,
pages: 0,
error: ""
};
}
componentDidMount() {
this.fetchPosts(this.state.page);
}
async fetchPosts(page) {
var self = this;
try {
const response = await axios.get('/posts', {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
data: { page: page },
//data: { page: this.state.page },
});
if (response.status >= 200 && response.status < 300) {
self.setState({ posts: response.data.posts, pages: parseInt(response.data.pages),
page: parseInt(response.data.page) });
} else {
self.setState({ error: response.data.error});
}
} catch (error) {
console.error(error);
}
}
handleChangePage(page) {
this.fetchPosts(page);
}
render() {
var posts = this.state.posts.map(post => {
return (
<div key={post.id}>
<h1>{post.id}</h1>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
});
return (
<div>
{posts}
<Pagination page={this.state.page}
pages={this.state.pages}
handleChangePage={this.handleChangePage} />
</div>
);
}
}
export default Posts;
in the trigger
constructor(props){
super(props)
...
this.handleChangePage = this.handleChangePage.bind(this)
}
Or you change function handleChangePage as below:
handleChangePage = (page) => {
...
}
Recommend:
You should use bind(this) for function in component. You can declare in contructor.
Or use arrow function

Pass state updater clickHandler in React

I have a React app like:
Main.js-
import React, { Component } from 'react';
import _ from 'underscore';
import ApplicationsButtons from '../components/ApplicationsButtons';
let applications_url = 'http://127.0.0.1:8889/api/applications'
export default class Main extends Component {
constructor(props) {
super(props);
this.state = {applications: [], selected_app: 1};
this.updateSelectedApp = this.updateSelectedApp.bind(this);
}
componentDidMount() {
let self = this;
$.ajax({
url: applications_url,
method: 'GET',
success: function(data) {
console.log(data);
let objects = data.objects;
let apps = objects.map(function(object) {
return {name: object.name, id: object.id};
});
console.log(apps);
self.setState({applications: apps});
}
});
}
updateSelectedApp(id) {
this.setState({selected_app: id});
}
render() {
return (
<div>
{this.state.selected_app}
<ApplicationsButtons apps={this.state.applications} />
</div>
);
}
}
ApplicationsButtons.js-
import React, { Component } from 'react';
export default class ApplicationsButtons extends Component {
render() {
var buttons = null;
let apps = this.props.apps;
let clickHandler = this.props.clickHandler;
if (apps.length > 0) {
buttons = apps.map(function(app) {
return (<button key={app.id}>{app.name} - {app.id}</button>);
// return (<button onClick={clickHandler.apply(null, app.id)} key={app.id}>{app.name} - {app.id}</button>);
});
}
return (
<div>
{buttons}
</div>
);
}
}
I want to pass an onClick to the buttons that will change the currently selected app. Somehow, I just got my first infinite loop in React ("setState has just ran 20000 times"). Apparently, when I tried to pass the event handler to be called on click, I told it to keep calling it.
The onClick function should change state.selected_app for the Main component, based on the id for the button that was clicked.
You are not passing the handler as prop.
Here's what you should do:
render() {
return (
<div>
{this.state.selected_app}
<ApplicationsButtons
apps={this.state.applications}
handleClick={this.updateSelectedApp}
/>
</div>
);
}
And in ApplicationButtons:
render() {
var buttons = null;
let apps = this.props.apps;
let clickHandler = this.props.handleClick;
if (apps.length > 0) {
buttons = apps.map(app =>
<button key={app.id} onClick={() => clickHandler(app.id)}>{app.name} - {app.id}</button>);
);
}
return (
<div>
{buttons}
</div>
);
}

Categories