MY QUESTION: Why doesn't updating a property of an object in an array in my Immutable state (Map) not cause Redux to update my component?
I'm trying to create a widget that uploads files to my server, and my initial state (from inside my UploaderReducer which you will see below) object looks like this:
let initState = Map({
files: List(),
displayMode: 'grid',
currentRequests: List()
});
I have a thunk method that starts uploads and dispatches actions when an event occurs (such as a progress update). For example, the onProgress event looks like this:
onProgress: (data) => {
dispatch(fileUploadProgressUpdated({
index,
progress: data.percentage
}));
}
I'm using redux-actions
to create and handle my actions, so my reducer for that action looks like this:
export default UploaderReducer = handleActions({
// Other actions...
FILE_UPLOAD_PROGRESS_UPDATED: (state, { payload }) => (
updateFilePropsAtIndex(
state,
payload.index,
{
status: FILE_UPLOAD_PROGRESS_UPDATED,
progress: payload.progress
}
)
)
}, initState);
And updateFilePropsAtIndex
looks like:
export function updateFilePropsAtIndex (state, index, fileProps) {
return state.updateIn(['files', index], file => {
try {
for (let prop in fileProps) {
if (fileProps.hasOwnProperty(prop)) {
if (Map.isMap(file)) {
file = file.set(prop, fileProps[prop]);
} else {
file[prop] = fileProps[prop];
}
}
}
} catch (e) {
console.error(e);
return file;
}
return file;
});
}
So far, this all seems to work fine! In Redux DevTools, it shows up as an action as expected. However, none of my components update! Adding new items to the files
array re-renders my UI with the new files added, so Redux certainly doesn't have a problem with me doing that...
My top level component that connects to the store using connect
looks like this:
const mapStateToProps = function (state) {
let uploadReducer = state.get('UploaderReducer');
let props = {
files: uploadReducer.get('files'),
displayMode: uploadReducer.get('displayMode'),
uploadsInProgress: uploadReducer.get('currentRequests').size > 0
};
return props;
};
class UploaderContainer extends Component {
constructor (props, context) {
super(props, context);
// Constructor things!
}
// Some events n stuff...
render(){
return (
<div>
<UploadWidget
//other props
files={this.props.files} />
</div>
);
}
}
export default connect(mapStateToProps, uploadActions)(UploaderContainer);
uploadActions
is an object with actions created using redux-actions
.
A file
object in the files
array is basically this:
{
name: '',
progress: 0,
status
}
The UploadWidget
is basically a drag n drop div and a the files
array printed out on the screen.
I tried using redux-immutablejs
to help out as I've seen in many posts on GitHub, but I have no idea if it helps... This is my root reducer:
import { combineReducers } from 'redux-immutablejs';
import { routeReducer as router } from 'redux-simple-router';
import UploaderReducer from './modules/UploaderReducer';
export default combineReducers({
UploaderReducer,
router
});
My app entry point looks like this:
const store = configureStore(Map({}));
syncReduxAndRouter(history, store, (state) => {
return state.get('router');
});
// Render the React application to the DOM
ReactDOM.render(
<Root history={history} routes={routes} store={store}/>,
document.getElementById('root')
);
Lastly, my <Root/>
component looks like this:
import React, { PropTypes } from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router';
export default class Root extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired,
routes: PropTypes.element.isRequired,
store: PropTypes.object.isRequired
};
get content () {
return (
<Router history={this.props.history}>
{this.props.routes}
</Router>
);
}
//Prep devTools, etc...
render () {
return (
<Provider store={this.props.store}>
<div style={{ height: '100%' }}>
{this.content}
{this.devTools}
</div>
</Provider>
);
}
}
So, ultimately, if I try to update a 'progress' in the following state object, React/Redux does not update my components:
{
UploaderReducer: {
files: [{progress: 0}]
}
}
Why is this? I thought the whole idea of using Immutable.js was that it was easier to compare modified objects regardless of how deeply you update them?
It seems generally getting Immutable to work with Redux is not as simple as it seems: How to use Immutable.js with redux? https://github.com/reactjs/redux/issues/548
However, the touted benefits of using Immutable seem to be worth this battle and I'd LOVE to figure out what I'm doing wrong!
UPDATE April 10 2016
The selected answer told me what I was doing wrong and for the sake of completeness, my updateFilePropsAtIndex
function now contains simply this:
return state.updateIn(['files', index], file =>
Object.assign({}, file, fileProps)
);
This works perfectly well! :)
Two general thoughts first:
- Immutable.js is potentially useful, yes, but you can accomplish the same immutable handling of data without using it. There's a number of libraries out there that can help make immutable data updates easier to read, but still operate on plain objects and arrays. I have many of them listed on the Immutable Data page in my Redux-related libraries repo.
- If a React component does not appear to be updating, it's almost always because a reducer is actually mutating data. The Redux FAQ has an answer on that topic, at http://redux.js.org/docs/FAQ.html#react-not-rerendering.
Now, given that you are using Immutable.js, I'll admit that mutation of data seems a bit unlikely. That said... the file[prop] = fileProps[prop]
line in your reducer does seem awfully curious. What exactly are you expecting to be going on there? I'd take a good look at that part.
Actually, now that I look at it... I am almost 100% certain that you are mutating data. Your updater callback to state.updateIn(['files', index])
is returning the exact same file object you got as a parameter. Per the Immutable.js docs at https://facebook.github.io/immutable-js/docs/#/Map:
If the updater function returns the same value it was called with, then no change will occur. This is still true if notSetValue is provided.
So yeah. You're returning the same value you were given, your direct mutations to it are showing up in the DevTools because that object is still hanging around, but since you returned the same object Immutable.js isn't actually returning any modified objects further up the hierarchy. So, when Redux does a check on the top-level object, it sees nothing has changed, doesn't notify subscribers, and therefore your component's mapStateToProps
never runs.
Clean up your reducer and return a new object from inside that updater, and it should all just work.
(A rather belated answer, but I just now saw the question, and it appears to still be open. Hopefully you actually got it fixed by now...)
来源:https://stackoverflow.com/questions/35676952/redux-not-updating-components-when-deep-immutable-state-properties-are-updated