Reputation: 31
I am trying to implement a process for adding or removing a song from a search results track list to the user's custom playlist.
I have added methods to App.js called addTrack
and removeTrack
that adds / removes a song to the playlist state. The application passes the method through a series of components to Track. The user can trigger the .addTrack()
or .removeTrack()
method by clicking the + or - sign from the search results list.
My issue is I do not know how to connect the anchor tag where I have my + and - to actually adding or removing the song from the custom playlist!
Any help you can give is greatly appreciate. Please see my code for App.js, SearchResults.js and Track below:
App.js
import React, { Component } from 'react';
import {Playlist} from '../Playlist/Playlist.js';
import './App.css';
import {SearchBar} from '../SearchBar/SearchBar.js';
import {SearchResults} from '../SearchResults/SearchResults.js';
const playlistName ="new playlist";
const playlistTracks = [{name:'biggie',artist:'biggiesmalls',album:'reaady to die'},{name:'nas',artist:'nases',album:'illmatic'},{name:'eminem',artist:'em',album:'marshall mathers'}];
class App extends Component {
constructor(props){
super(props);
this.state.searchResults = [{name:'biggiesearch',artist:'biggiesmallssearch',album:'reaady to diesearch'},{name:'nassearch',artist:'nasessearch',album:'illmaticsearch'},{name:'eminemsearch',artist:'emsearch',album:'marshall matherssearch'}]
this.addTrack = this.addTrack.bind(this);
this.removeTrack = this.removeTrack.bind(this);
}
addTrack(track){
if(track.id !== this.state.playlistTracks){
this.state.playlistTracks = this.state.playlistTracks.push(track);
}
}
removeTrack(track){
this.state.playlistTracks = this.state.playlistTracks.filter(track => track.id);
//this may be wrong! (point 49 in checklist)
}
render() {
return (
<div>
<h1>Ja<span className="highlight">mmm</span>ing</h1>
<div className="App">
<SearchBar onAdd={this.addTrack}/>
<div className="App-playlist">
<SearchResults searchResults={this.state.searchResults}/>
<Playlist
playlistName={this.state.playlistName}
playlistTracks={this.state.playlistTracks}
onRemove={this.state.removeTrack}/>
</div>
</div>
</div>
);
}
}
export default App;
Track.js
import React, { Component } from 'react';
import './Track.css';
export class Track extends Component {
constructor(props){
super(props);
this.addTrack = this.addTrack.bind(this);
this.removeTrack = this.removeTrack.bind(this);
}
renderAction(){
//className = track-action
if(Track.isRemoval === true){
return '+'
}
else{
return '-'
}
}
render() {
return (
<div className="Track">
<div className="Track-information">
<h3>{this.state.track.name}</h3>
<p>{this.state.track.artist} | {this.state.track.album}</p>
</div>
<a className="Track-action" >{Track.renderAction}</a>
</div>
);
}
}
export default Track;
SearchResults.js
import React, { Component } from 'react';
import './SearchResults.css';
import {TrackList} from '../TrackList/TrackList.js';
export class SearchResults extends Component {
render() {
return (
<div className="SearchResults">
<h2>Results</h2>
<TrackList tracks={this.props.searchResults} onAdd={this.props.onAdd}/>
</div>
);
}
}
export default SearchResults;
Tracklist
import React, { Component } from 'react';
import './TrackList.css';
import {Track} from '../Track/Track.js';
// const tracks = ['firstTrack','secondTrack','thirdTrack'];
export class TrackList extends Component {
render() {
return (
<div className="TrackList">
<Track
track={this.props.track}
onAdd={this.props.onAdd(this.props.track)}
onRemove={this.props.onRemove(this.props.track)}/>
const trackItems = {this.props.tracks}.map((track) =>
<li key={this.props.track.id}>
track
{this.props.track.name};
{this.props.track.artist};
{this.props.track.album};
</li>
);
</div>
);
}
}
export default TrackList;
Upvotes: 2
Views: 2553
Reputation: 3518
You need to use this.setState()
for the tracks to be added in your addTrack()
function.
addTrack(track){
let tracks = this.state.playlistTracks;
let trackExists = tracks.filter(t=>{return t.id==track.id}); //Check if already exists in array
if(trackExists.length === 0){
this.setState({
playlistTracks: this.state.playlistTracks.push(track)
});
}
}
Upvotes: 0
Reputation: 8980
You could solve your use-case on at least two ways:
It's like you've tried it in your code. And I think the only problem in your code is how your working with the state and the data structure behind it. Never modify state directly. e.g. this.state.playlist.push(...) is incorrect.
The only position where you can write this.state = { }
is in a constructor. Later you have to always create a new state object with some changes to the previous state and use this.setState({...})
.
With this approach you should have your playlist that you're modifying in the parent component's state and the callbacks will create a new state based on the parameter you're passing.
The callbacks are used to connect the child component with the parent. The context/scope inside them is set to the App component - so this.setState(...)
will modify the state of App component. It's done with .bind(this)
inside the constructor.
So I'll give you some info to my demo code that is using Redux (see below demo or fiddle). But also check the docs (Redux & React-Redux) for more details as I can only give you an overview here:
ReactDOM.render(
<Provider store={store}>
<AppContainer />
</Provider>
, document.getElementById('root'));
We're using the Provider
component to wrap our App as children into it so we don't have to manually pass the store around. This is handled by React-Redux for us. The store is available as context in every component that's inside the Provider - even nested components.
You usually don't use the context directly because React-Redux has a helper that you can and should use. It's called connect
. With it as it is already saying you can connect your component to Redux store and also map your state & methods to your component properties.
React-Redux's connect is doing a lot of stuff for you e.g. subscribing the playlist prop to watch store changes and triggers a component re-render on data change. You can try to add the same behavior with-out connect so you can learn more about it - see commented parts in demo.
To get started with reducers it is OK to start with just one reducer. Later you can combine mulitple reducers e.g. you could create a playlistReducer
.
What is a reducer doing? It's a pure javascript function that is getting an action and will return the next state based on the action and the previous state. Always remember to create a new state and don't modify the state directly. The action will usually contain an type and some payload.
Pure function means that it will always create the same state if you're passing the same data to it and the initial state is the same. So avoid ajax requests inside a reducer. A reducer needs to be free of side-effect.
Pass the rootReducer
to createStore
from Redux. It will return the store object that you're passing to the provider mentioned above.
It's needed to connect the Redux state and dispatchers to props of your app component.
I would use Redux as playlist managing can quickly become more complicated e.g. multiple lists etc. If you're only having one playlist it's also OK to do it with parent / child approach.
Please have a look at the demos below or the following fiddles:
Parent / child demo code:
const log = (val) => JSON.stringify(val, null, 2);
const Track = (props) => {
const displayIcon = (type) => {
const adding = type === 'add';
return (
<span title={adding? 'add track' : 'remove track'}>{adding ? '+' : '-'}</span>
)
};
return (
<span onClick={() => props.clickHandler(props.track)}>
{displayIcon(props.type)} {props.track.title ||props.track.name} - {props.track.artist}
</span>
)
}
const Tracklist = (props) => {
const listType = props.listType;
return (
<div>
{props.playlist.length === 0 ?
( <strong>No tracks in list yet.</strong> ) :
( <ul>
{ props.playlist.map((track) =>
(
<li key={track.id} >
{ listType === 'playlist' ?
<Track
clickHandler={props.clickHandler}
type="remove"
track={track} /> :
<Track
clickHandler={props.clickHandler}
type="add"
track={track} /> }
<span>{props.isInList && props.isInList(track) ?
' - added' : null
}
</span>
</li>
)
)}
</ul> )
}
</div>
)
}
const initialState = {
playlist: [{
id: 0,
title:'Black or White',
artist: 'Michael Jackson'
},
{
id: 1,
title:'Bad',
artist: 'Michael Jackson'
},
]
};
const searchData = {
playlist: [
{id: 's1', name:'biggiesearch',artist:'biggiesmallssearch',album:'reaady to diesearch'},
{id: 's2', name:'nassearch',artist:'nasessearch',album:'illmaticsearch'},
{id: 's3', name:'eminemsearch',artist:'emsearch',album:'marshall matherssearch'}
]
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = initialState;
this.add = this.add.bind(this);
this.remove = this.remove.bind(this);
this.isInList = this.isInList.bind(this);
}
render () {
return (
<div>
<h1>Playlist:</h1>
<Tracklist
listType="playlist"
playlist={this.state.playlist}
clickHandler={this.remove}
/>
<h1>Search result:</h1>
<Tracklist
playlist={searchData.playlist}
clickHandler={this.add}
isInList={this.isInList}
/>
<pre>{log(this.state)}</pre>
</div>
)
}
add (newTrack) {
console.log('Add track', newTrack);
if (this.state.playlist.filter(track => track.id === newTrack.id).length === 0) {
this.setState({
...this.state,
playlist: [
...this.state.playlist,
newTrack
]
});
}
}
isInList (track) {
// used for displayling if search result track is already in playlist
return this.state.playlist.filter(playlistTrack =>
playlistTrack.id === track.id).length > 0;
}
remove (trackToRemove) {
console.log('remove', trackToRemove);
this.setState({
...this.state,
playlist: this.state.playlist.filter(track => track.id !== trackToRemove.id)
});
}
}
ReactDOM.render(
<App/>,
document.getElementById('root')
)
* {
font-family: sans-serif;
}
h1 {
font-size: 1.2em;
}
li {
list-style-type: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.0.0/umd/react-dom.development.js"></script>
<div id="root"></div>
Redux demo
const TrackList = (props) => (
<div>
{props.tracks.map((track) => (
<div key={track.id}>{track.title || track.name} - {track.artist}
{props.onAddClick ?
( <button onClick={() => props.onAddClick(track)}>Add</button> ) : null }
{props.onRemoveClick ?
( <button onClick={() => props.onRemoveClick(track.id)}>Remove</button> ): null
}
</div>
))}
{props.tracks.length === 0 ?
( <strong>No tracks.</strong> ) : null
}
</div>
)
/*
const App = ({playlist, add, remove}) => (
<div>
<h2>Current playlist</h2>
<TrackList tracks={playlist} onRemoveClick={remove}></TrackList>
<SearchResult onAddClick={add}></SearchResult>
</div>
)*/
class App extends React.Component {
render () {
/*
// the following code would be required if connect from react-redux is not used
// -> subscribe to state change and update component
// So always use React-redux's connect to simplify code
const store = this.context.store;
console.log(this.props);
const select = (state) => state.playlist;
let playlist = select(store.getState());
function handleChange() {
let previousValue = playlist
playlist = select(store.getState())
if (previousValue !== playlist) {
console.log('playlist changed');
// re-render
this.forceUpdate();
}
}
this.unsubscribe = store.subscribe(handleChange.bind(this));
// --> also unsubscribing in unmount would be required
*/
console.log('playlist render', this.props);
return (
<div>
<h2>Current playlist</h2>
<TrackList tracks={this.props.playlist} onRemoveClick={this.props.remove}></TrackList>
<SearchResult onAddClick={this.props.add}></SearchResult>
<hr/>
<pre>
debugging
{JSON.stringify(store.getState(), null, 2)}
</pre>
</div>
)
}
}
console.log('react', PropTypes.object);
App.contextTypes = {
store: PropTypes.object
}
class SearchResult extends React.Component {
constructor(props) {
super(props);
this.searchResults = // this will be a result of a search later
[
{id: 's1', name:'biggiesearch',artist:'biggiesmallssearch',album:'reaady to diesearch'},
{id: 's2', name:'nassearch',artist:'nasessearch',album:'illmaticsearch'},
{id: 's3', name:'eminemsearch',artist:'emsearch',album:'marshall matherssearch'}
];
}
render () {
return (
<div>
<h2>Search results: </h2>
<TrackList tracks={this.searchResults} onAddClick={this.props.onAddClick}>
</TrackList>
</div>
)
}
}
const initialState = {
playlist: [{
id: 0,
title:'Michal Jackson',
artist: 'Black or White'
},
{
id: 1,
title:'Michal Jackson',
artist: 'Bad'
},
]
}
const rootReducer = (state = initialState, action) => {
const newTrack = action.track;
switch(action.type) {
case 'ADD':
// only add once
if (state.playlist.filter(track => action.track.id === track.id).length > 0) {
return state; // do nothing --> already in list
}
return {
...state,
playlist: [
...state.playlist,
{
id: state.playlist.length,
...newTrack
}
]
};
case 'REMOVE':
console.log('remove', action.id)
return {
...state,
playlist: state.playlist.filter((track) => track.id !== action.id)
};
default:
return state;
}
}
const store = Redux.createStore(rootReducer)
const Provider = ReactRedux.Provider
// actions
const addToList = (track) => {
return {
type: 'ADD',
track
};
}
const removeFromList = id => {
return {
type: 'REMOVE',
id
};
}
const AppContainer = ReactRedux.connect(
// null // --> set to null if you're not using mapStateToProps --> manually handling state changes required (see comment in App)
(state) => { return { playlist: state.playlist } } // mapStateToProps
,
(dispatch) => {
return {
add: track => dispatch(addToList(track)), // mapDispatchToProps
remove: id => dispatch(removeFromList(id))
}
}
)(App)
console.log(Provider);
ReactDOM.render(
<Provider store={store}>
<AppContainer />
</Provider>
, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.development.js"></script>
<script src="https://unpkg.com/prop-types/prop-types.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.6/react-redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.0.0/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.7.2/redux.js"></script>
<div id="root">
</div>
Upvotes: 3