I'm working on small drag and drop app.
I want to be able to persist it's state after page refresh so I thought about using Redux and persisting chosen keys.
I'm using React-dnd and have drag and drop logic already in place. On some events (e.g. drop) I've added actions to be dispatched. These actions change redux state and move chosen items from one list to another. I can see the changes in redux store but they don't appear in the UI. Basically props doesn't change even though Redux state does change. At the same time using the same selectors I receive correct initial state of my UI.
My issue: props don't update on state change.
My question: why and what am I doing wrong?
MaveISeenItContainer.js
import React, {Component} from 'react';
import {connect} from 'react-redux';
import HTML5Backend from 'react-dnd-html5-backend';
import {DragDropContext} from 'react-dnd';
import * as actions from '../../actions/MoviesActions';
import HaveISeenItComponent from './HaveISeenItComponent';
import {getSelectedMovies, getAllMovies, getStatus} from '../../reducers/MoviesReducer';
let HaveISeenItContainer = props => <HaveISeenItComponent {...props} />;
const mapDispatchToProps = dispatch => ({
getMovies: () => dispatch(actions.getMovies.request()),
selectMovie: values => dispatch(actions.selectMovie.request(values)),
removeMovie: values => dispatch(actions.removeMovie.request(values))
});
const mapStateToProps = state => ({
allMovies: getAllMovies(state),
selectedMovies: getSelectedMovies(state),
status: getStatus(state)
});
HaveISeenItContainer = DragDropContext(HTML5Backend)(HaveISeenItContainer);
export default connect(mapStateToProps, mapDispatchToProps)(HaveISeenItContainer);
HaveISeenItComponent.js
class HaveISeenItComponent extends Component {
componentDidMount(id, index) {
if (this.props.status !== 'SUCCESS') {
this.props.getMovies();
}
}
handleMovieSelect = (id, card) => {
this.props.selectMovie({listId: id, id: card.id, card: card})
};
handleMovieRemove = (id, index) => {
this.props.removeMovie({listId: id, cardId: index});
};
render() {
const {selectedMovies, allMovies} = this.props;
let availableMoviesList = null;
let moviesToBeSeenList = null;
if (this.props.status === 'SUCCESS') {
const allMoviesList = _.map(allMovies, (value, key) => ({
id: key,
title: value.title
})
);
const selectedMoviesList = _.map(selectedMovies, (value, key) => ({
id: key,
title: value.title
})
);
availableMoviesList = <List
id={1}
list={allMoviesList}
onMovieRemove={this.handleMovieRemove}
onMovieSelect={this.handleMovieSelect}
/>;
moviesToBeSeenList = <List
id={2}
list={selectedMoviesList}
onMovieRemove={this.handleMovieRemove}
onMovieSelect={this.handleMovieSelect}
/>;
}
return (
<div className={styles.mainComponent}>
{moviesToBeSeenList}
{availableMoviesList}
</div>
);
}
}
HaveISeenItComponent.propTypes = {
getMovies: PropTypes.func
};
export default HaveISeenItComponent;
List.js
class List extends Component {
constructor(props) {
super(props);
this.state = {cards: props.list};
}
pushCard(card) {
const {onMovieSelect, id} = this.props;
onMovieSelect(id, card);
}
removeCard(index) {
const {onMovieRemove, id} = this.props;
onMovieRemove(id, index);
}
moveCard(dragIndex, hoverIndex) {
const {cards} = this.state;
const dragCard = cards[dragIndex];
this.setState(update(this.state, {
cards: {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard]
]
}
}));
}
render() {
const {cards} = this.state;
const {canDrop, isOver, connectDropTarget} = this.props;
const isActive = canDrop && isOver;
const style = {
width: "200px",
height: "404px",
border: '1px dashed gray'
};
const backgroundColor = isActive ? 'lightgreen' : '#FFF';
return connectDropTarget(
<div style={{...style, backgroundColor}}>
{cards.map((card, i) => {
return (
<Card
key={card.id}
index={i}
listId={this.props.id}
card={card}
removeCard={this.removeCard.bind(this)}
moveCard={this.moveCard.bind(this)}/>
);
})}
</div>
);
}
}
const cardTarget = {
drop(props, monitor, component) {
const {id} = props;
const sourceObj = monitor.getItem();
if (id !== sourceObj.listId) component.pushCard(sourceObj.card);
return {
listId: id
};
}
}
export default DropTarget("Card", cardTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
}))(List);
MoviesReducer.js (with basic selectors)
import _ from "lodash";
// import * as actions from 'actions/MoviesActions';
// import { REQUEST_STATUS } from "constants/types";
import * as actions from '../actions/MoviesActions';
import {REQUEST_STATUS} from '../constants/types';
import {LIST_INDEX} from '../constants/constants';
const initialState = {
allMovies: [],
selectedMovies: []
};
const MoviesReducer = (state = initialState, action) => {
switch(action.type){
case actions.GET_MOVIES.REQUEST:
return {
...state,
status: REQUEST_STATUS.PENDING
};
case actions.GET_MOVIES.SUCCESS:
return {
...state,
status: REQUEST_STATUS.SUCCESS,
allMovies: action.response.movies
};
case actions.GET_MOVIES.FAILURE:
return {
...state,
status: REQUEST_STATUS.FAILURE
};
case actions.SELECT_MOVIE.REQUEST:
if(action.values.listId === LIST_INDEX.SELECTED_MOVIES){
state.selectedMovies.splice(action.values.cardId, 0, action.values.card);
} else if (action.values.listId === LIST_INDEX.ALL_MOVIES){
state.allMovies.splice(action.values.cardId, 0, action.values.card);
}
return state;
case actions.REMOVE_MOVIE.REQUEST:
if(action.values.listId === LIST_INDEX.SELECTED_MOVIES){
state.selectedMovies.splice(action.values.cardId, 1);
} else if (action.values.listId === LIST_INDEX.ALL_MOVIES){
state.allMovies.splice(action.values.cardId, 1);
}
return state;
case actions.MOVE_MOVIE.REQUEST:
return state;
default:
return state;
}
};
export const MOVIES_STATE_KEY = 'moviesState';
// Selectors
export const getStatus = state => _.get(state, [MOVIES_STATE_KEY, 'status']);
export const getAllMovies = state => _.get(state, [MOVIES_STATE_KEY, 'allMovies']);
export const getSelectedMovies = state => _.get(state, [MOVIES_STATE_KEY, 'selectedMovies']);
export default MoviesReducer;