Code Randomly Changing a State Element to 1 - javascript

Working on a project that will allow you to search the Spotify database for songs, add it to a playlist, then add the playlist to your account, I run into an error when attempting to add a song to a playlist. Upon searching through the code to find and reason, and adding console logs to see where it goes wrong. It seems that in the render statement, the state element playlistTracks is changed from an Array with the added song inside it as an object to just the integer 1. This image shows the progression of console logs:
This is the code from App.js:
import React from 'react';
import './App.css';
import SearchBar from '../SearchBar/SearchBar';
import SearchResults from '../SearchResults/SearchResults';
import Playlist from '../Playlist/Playlist';
import Spotify from '../../util/Spotify.js';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
searchResults: [],
playlistName: 'New Playlist',
playlistTracks: []
}
this.addTrack = this.addTrack.bind(this);
this.removeTrack = this.removeTrack.bind(this);
this.updatePlaylistName = this.updatePlaylistName.bind(this);
this.savePlaylist = this.savePlaylist.bind(this);
this.search = this.search.bind(this);
}
addTrack(track) {
console.log(track);
if(this.state.playlistTracks.length !== 0) {
const ids = Playlist.collectIds(this.state.playlistTracks);
let newId = true;
for(let i = 0; i < ids.length; i++) {
if(ids[i] === track.id) {
newId = false;
}
}
if(newId) {
this.setState({playlistTracks: this.state.playlistTracks.push(track)});
}
} else {
this.setState({playlistTracks: this.state.playlistTracks.push(track)});
}
console.log(this.state.playlistTracks);
}
removeTrack(track) {
const ids = Playlist.collectIds(this.state.playlistTracks);
let trackIndex = -1;
for(let i = 0; i < ids.length; i++) {
if (ids[i] === track.id) {
trackIndex = i;
}
}
if (trackIndex !== -1) {
const newPlaylist = this.state.playlistTracks.splice(trackIndex, 1);
this.setState({playlistTracks: newPlaylist});
}
}
updatePlaylistName(name) {
this.setState({playlistName: name});
}
savePlaylist() {
let trackURIs = [];
for(let i = 0; i < this.state.playlistTracks.length; i++) {
trackURIs.push(this.state.playlistTracks[i].uri);
}
Spotify.savePlaylist(this.state.playlistName, trackURIs);
this.setState({playlistName: 'New Playlist', playlistTracks: []});
}
async search(term) {
const results = await Spotify.search(term);
this.setState({searchResults: results});
}
render() {
return (
<div id="root">
<h1>Ja<span className="highlight">mmm</span>ing</h1>
<div className="App">
<SearchBar onSearch={this.search} />
<div className="App-playlist">
{console.log(this.state.playlistTracks)}
<SearchResults searchResults={this.state.searchResults} onAdd={this.addTrack} />
<Playlist
playlistName={this.state.playlistName}
playlistTracks={this.state.playlistTracks}
onRemove={this.removeTrack}
onNameChange={this.updatePlaylistName}
onSave={this.savePlaylist}
/>
</div>
</div>
</div>
);
}
}
export default App;

The error is likely a result of pushing and mutating state in one line.
Try:
// Add Track.
addTrack = (track) => this.setState({playlistTracks: [...this.state.playlistTracks, track]})

Array.prototype.push returns the new number of elements in the array. It does not return the array itself. You are setting the state to the returned value, which is 1.
Here are some examples to illustrate what's happening:
What you expect to happen:
let someArray = [];
someArray.push('something');
console.log(someArray); // -> ['something']
But what you are actually doing is akin to:
let someArray = [];
someArray = someArray.push('something');
console.log(someArray); // -> 1

Related

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.

Why is my for loop exiting before my condition

import ReactDOM from "react-dom";
import React, { useState } from "react";
const App = () => {
let [persons, setPersons] = useState([{ id: 11, name: "Arto Hellas" }]);
const [newName, setNewName] = useState("");
const check = (arr, name) => {
let i = 0;
let checking = true;
for (i = 0; i < arr.length; i++) {
console.log(arr[i].name === name);
if (arr[i].name === name) {
checking = false;
break;
}
console.log(i);
return checking;
}
};
const addName = event => {
event.preventDefault();
const nameObject = {
id: newName.length + 1,
name: newName
};
check(persons, nameObject.name)
? setPersons((persons = persons.concat(nameObject)))
: window.alert(`${nameObject.name} is already listed`);
setNewName("");
console.log(JSON.stringify(persons));
};
const handleNameChange = event => {
setNewName(event.target.value);
};
return (
<div>
<h2>Phonebook</h2>
<form onSubmit={addName}>
<div>
name: <input value={newName} onChange={handleNameChange} />
</div>
<div>
<button type="submit">add</button>
</div>
</form>
<h2>Numbers</h2>
<ul>
{persons.map(person => (
<li key={person.id}>{person.name} </li>
))}
</ul>
</div>
);
};
export default App;
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
The problem is with my check function which checks if a name exists in the array. I have a for loop that iterates throughout the array but it always stops early. I checked this with console.log(i). If you add Adib once the array looks like this [{"id":11,"name":"Arto Hellas"},{"id":5,"name":"Adib"}] and the value of i is 0 since because before this the length of arr was 1. However if you add Adib again it will do so and value of i is 0 again and not 1
You have return checking in loop. Just put it after }:
const check = (arr, name) => {
let i = 0;
let checking = true;
for (i = 0; i < arr.length; i++) {
console.log(arr[i].name === name);
if (arr[i].name === name) {
checking = false;
break;
}
console.log(i);
}
return checking;
};
See full example in playground: https://jscomplete.com/playground/s522611
I'm sorry because I'm not on VScode right now I don't have a plugin which lets me see my brackets more clearly and accidentally had my return statement in a for loop.

How to call a rest api and feed the result into a subsequent promise call

I have the following code that calls a rest api and then uses the resultant data and feeds the values into a subsequent api call. Not sure how to make this work..! You can see my comment in the second method and this displays the data, but because it's a promise I'm not sure how to pass that back?
Any ideas?
Thanks
Code snippet
componentDidMount() {
myMethod();
}
getBookings(id) {
getCustomerBookings(id).then(res => {
console.log(res); // displays the data correctly
return res;
});
}
myMethod() {
var self = this;
var myArray = [];
getCustomers().then(result => {
for(var index = 0; index < result.length; index++) {
myArray.push(<div className="col">
{result[index].customerId} // displays customer id as expected
{this.getBookings(result[index].customerId)} // attempt
</div>
self.setState({customers: myArray});
});
}
You could split this the single component into two - CustomerContainer and Customer, and tie the second API call to the mounting of the Customer component.
import React, { Component } from 'react';
class CustomerContainer extends Component {
constructor() {
super();
this.state = {
customers = []
}
}
async getCustomers() {
// fetch customers
}
async componentDidMount() {
customers = await this.getCustomers();
this.setState({ customers })
}
render() {
const { customers } = this.state
return (
{customers.length > 0 && customers.map(customer => {
return <Customer customerId= {customer.id} />
})}
)
}
}
class Customer extends Component {
constructor() {
super();
this.state = {
bookings = [];
}
}
async getBookings(id) {
// fetch booking
}
async componentDidMount() {
bookings = await this.getBookings(this.props.customerId);
this.setState({ bookings })
}
render() {
const { bookings } = this.state;
return (
<div className="col">
{this.props.customerId} // displays customer id as expected
</div>
)
}
}
As you are doing with getCustomers(), you have to get the result of the promise with then. So your code would look like this:
myMethod() {
var self = this;
var myArray = [];
getCustomers().then(result => {
for(var index = 0; index < result.length; index++) {
this.getBookings(result[index].customerId).then(bookings => {
myArray.push(<div className="col">
{result[index].customerId}
{bookings}
</div>);
});
}
self.setState({customers: myArray});
});
}
Note that this solution assumes you are not using ES6 async/await structure. Otherwise the other answers are better.
How about this
async myMethod() {
var self = this;
var myArray = [];
var result = await getCustomers();
for(var index = 0; index < result.length; index++) {
var booking = await this.getBookings(result[index].customerId);
myArray.push(<div className="col">
{result[index].customerId}
{booking}
</div>
}
self.setState({customers: myArray});
}

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

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

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

Categories