You could solve your use-case on at least two ways:
- Parent and child components where the child will trigger a callback of the parent. (This is your approach of your snippets.) See demo or fiddle below.
- Redux to manage the playlist as part of your application state.
To 1. Parent/Child components
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.
To 2. Redux
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:
Bootstrapping your app
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.
Root reducer
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.
Create the Redux store
Pass the rootReducer
to createStore
from Redux. It will return the store object that you're passing to the provider mentioned above.
Create an AppContainer Component
It's needed to connect the Redux state and dispatchers to props of your app component.
Final thoughts / recommendation
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>