Jamming - Step 95: Uncaught TypeError: Cannot read properties of undefined (reading 'then')

Hi all, I am working on the Jammming project in the Full Stack Engineer career path and have had an issue that others appear to have encountered, but sadly none of the related solutions have worked for them.

My error occurs when I try to save a playlist to Spotify with a name that is different from the default (the default name works). While most people have had issues in their Spotify components, I think mine actually has to do with my Playlist component but I cannot figure it out.

In terms of steps I have taken to troubleshoot so far I have:

  • Reviewed the walkthrough video step by step
  • Read through ~20-30 threads related to this issue and checked my code for the solutions used in those instances.

I would be enormously grateful if you could review my code and advise. It is as follows:

App.js:

import './App.css';
import React from 'react';
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: 'My 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) {
        if (this.state.playlistTracks.includes(track.id)) {
            return;
        } else {
            const playlistUpdate = this.state.playlistTracks;
            playlistUpdate.push(track);
            this.setState({ playlistTracks: playlistUpdate });
        }
    }

    removeTrack(track) {
        let tracks = this.state.playlistTracks;
        tracks = tracks.filter(currentTrack => currentTrack.id !== track.id);

        this.setState({ playlistTracks: tracks });
    }

    updatePlaylistName(name) {
        this.setState({ playlistName: name });
    }

    savePlaylist() {
        const trackUris = this.state.playlistTracks.map(track => track.uri);
        Spotify.savePlaylist(this.state.playlistName, trackUris).then(() => {
            this.setState({
                playlistName: 'New Playlist',
                playlistTracks: []
            });
        });
    }

    search(term) {
        Spotify.search(term).then(searchResults => {
            this.setState({ searchResults: searchResults });
        });
    }

    render() {
        return (
            <div>
                <h1>Ja<span className="highlight">mmm</span>ing</h1>
                <div className="App">
                    <SearchBar onSearch={this.search} />

                    <div className="App-playlist">
                        <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;

Spotify.js:

const clientId = 'REDACTED';
const redirectUri = 'http://localhost:3000';
let accessToken;

const Spotify = {

    getAccessToken() {
        if (accessToken) {
            return accessToken
        } 

        //check for access token match
        const accessTokenMatch = window.location.href.match(/access_token=([^&]*)/);
        const expiresInMatch = window.location.href.match(/expires_in=([^&]*)/);

        if (accessTokenMatch && expiresInMatch) {
            accessToken = accessTokenMatch[1];
            const expiresIn = Number(expiresInMatch[1]);
            //This is going to clear the parameters and allow us to grab a new access token when it expires.
            window.setTimeout(() => accessToken = '', expiresIn * 1000);
            window.history.pushState('Access Token', null, '/');
            return accessToken
        } else {
            const accessUrl = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=token&scope=playlist-modify-public&redirect_uri=${redirectUri}`;
            window.location = accessUrl;
        }
    },

    search(search) {
        const accessToken = Spotify.getAccessToken();
        return fetch(`https://api.spotify.com/v1/search?type=track&q=${search}`, {
            headers: {
                Authorization: `Bearer ${accessToken}`
            }
        }).then(response => {
            return response.json();
        }).then(jsonResponse => {
            if (!jsonResponse.tracks) {
                return [];
            }
            return jsonResponse.tracks.items.map(track => ({
                id: track.id,
                name: track.name,
                artist: track.artists[0].name,
                album: track.album.name,
                uri: track.uri
            }));
        });
    },

    savePlaylist(name, trackUris) {
        if (!name || !trackUris.length) {
            return;
        }

        const accessToken = Spotify.getAccessToken();
        const headers = { Authorization: `Bearer ${accessToken}` };
        let userId;

        return fetch(`https://api.spotify.com/v1/me`, { headers: headers }
        ).then(response => response.json()
        ).then(jsonResponse => {
            userId = jsonResponse.id;
            return fetch(`https://api.spotify.com/v1/users/${userId}/playlists`,
                {
                    headers: headers,
                    method: 'POST',
                    body: JSON.stringify({ name: name })
                }).then(response => response.json()
                ).then(jsonResponse => {
                    const playlistId = jsonResponse.id;
                    return fetch(`https://api.spotify.com/v1/users/${userId}/playlists/${playlistId}/tracks`, {
                        headers: headers,
                        method: 'POST',
                        body: JSON.stringify({ uris: trackUris })
                    });
                });
        });
    }
}

export default Spotify;

Playlist.js:

import React from 'react';
import './Playlist.css';
import TrackList from '../TrackList/TrackList';

class Playlist extends React.Component {
    constructor(props) {
        super(props);
        this.handleNameChange = this.handleNameChange.bind(this);
    }


    handleNameChange(event) {
        this.props.onNameChange(event.target.onChange)
    }

    render() {
        return (
            <div className="Playlist">
                <input defaultValue={'New Playlist'}
                       onChange={this.handleNameChange} />
                <TrackList tracks={this.props.playlistTracks}
                           onRemove={this.props.onRemove}
                           isRemoval={true}/>
                <button className="Playlist-save"
                    onClick={this.props.onSave }>SAVE TO SPOTIFY</button>
            </div>
        )
    }
}

export default Playlist

In Playlist.js, the following doesn’t look right:

handleNameChange(event) {
        this.props.onNameChange(event.target.onChange)
        // Shouldn't it be?
        // this.props.onNameChange(event.target.value)
    }

In App.js, an instance of the Playlist component is being provided the property onNameChange={this.updatePlaylistName}

updatePlaylistName is defined as:

updatePlaylistName(name) {
        this.setState({ playlistName: name });
    }

In Playlist.js, the input element’s onChange attribute has been set as onChange={this.handleNameChange}.
When the text in the input field is changed, the event handler handleNameChange will be triggered and an event object will be provided as the argument. handleNameChange makes a call to the function assigned to the onNameChange property and provides it with an argument.
event.target.value seems to be the correct argument as opposed to event.target.onChange

From https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#value

1 Like

Thank you! I really appreciate you taking the time to help me out here - this solved my problem.

Can I ask two follow up questions?

  • Am I correct in understanding that what I was passing to handleNameChange was a property, when instead I needed to be passing it a value?
  • How did you diagnose that this was the mistake? The error thrown by the browser wasn’t very helpful in terms of tracing this back so it would be great to learn what steps you took to figure this out so I can do the same in future.

Sorry for the late reply.

You have an HTML input element (default input type is text). You want to use whatever text is typed into the input field to update the playlist name in the state. For a text input field,

The value attribute is a string that contains the current value of the text entered into the text field.

(How the value attribute works may differ based upon the type of the input element. See: HTML input value Attribute)

Since you are using a text input element, so event.target.value allows you to send the text entered in the input field to the onchange event handler (which then passes this text as the argument to the updatePlaylistName function).

You were passing event.target.onChange instead. I am not sure if that would pass a property. I just don’t know. You could create a new thread and ask how the expression would be interpreted. Hopefully, someone knowledgeable would chime in with a satisfactory explanation. I think if you did something like:

handleNameChange(event) {
        console.log(event.target.value);
        console.log(event.target.onChange);
        this.props.onNameChange(event.target.value)
    }

and inspected the console, the first console statement will log whatever you have typed into the input field. The second console statement will likely log undefined. But like I said, I don’t have a satisfactory explanation as to what exactly is happening.

One useful idea is to make use of the console (as mentioned by mirja_t in a number of threads). Use console statements at different points of the code and observe whether the output is as expected. It may help narrow down where something goes wrong.

You did a good job of identifying what was working and what wasn’t and also narrowing it down to the Playlist component being the likely issue.

In Playlist.js, the first thing would be to look for obvious syntax issues (spelling mistakes, missing braces etc.)
Your observation that the component works for default name, but breaks down for other names suggests that investigating the event handlers may be a good idea. I looked at the input element and since it is an HTML element and not a user-defined component, I was initially thrown off by the defaultValue attribute. After researching, it became clear that it is a perfectly valid attribute. I just hadn’t seen it before.

Then I looked at the onChange event handler. Looking at handleNameChange informed me that the component expected to be passed a property onNameChange holding the reference to some function and we are supposed to invoke this function with an argument provided by us. That led me to App.js where an instance of the Playlist component was being passed updatePlaylistName as the value for the onNameChange prop. This function updated the playlistName in the state object with whatever argument is passed to it. Like I said earlier, I don’t know what event.target.onChange exactly means or even if it is valid. If it isn’t undefined, then even in the best scenario event.target.onChange would hold a reference?/name? of the handleNameChange function. Clearly, this is not what we want to pass as the argument to updatePlaylistName. Instead event.target.value (being the text we type into the input field) is what we want to pass as the argument to updatePlaylistName, so that the state is updated appropriately.

Console/print statements at various points in the code can be useful. You have to conduct your own experiments and test out different theories as to why your code isn’t working.
If an unfamiliar error is being thrown, do a web search for the exact error. It can reveal possible causes for why the error is thrown (e.g. it may alert you to an import issue, some edge case, or provide some other lead to investigate).
Since many people attempt/complete the Codecademy exercises/projects, so you have the bonus that you can compare your code directly to other people’s codes. Of course for code other than Codecademy exercises/solutions, you won’t have that luxury. For exercises I have completed, I usually copy my code and paste it in some free online text comparison tool (such as text-compare. If you do a web search for “online text compare tools” or “diff tools”, you will get plenty of free options). Then I copy paste the other person’s code and make a comparison. For subtle difference which may be hard to spot by eye, this is really useful in highlighting differences. I haven’t done this project so I don’t have any of my code to compare. But searching in Codecademy forums and/or github can allow you to directly compare your code to someone else’s code and spot any differences. That being said, in normal circumstances, you won’t have the luxury to compare codes and instead will have to surface and solve any bugs in the code yourself.

1 Like

Amazing response - thank you very much for taking the time to share your thoughts.

1 Like