Redux does not update components when updating immutable state properties

MY QUESTION: Why updating the property of an object in an array in my immutable state (Map) does not cause Redux to update my component?

I am trying to create a widget that uploads files to my server, and my initial state (from my UploaderReducer object, which you will see below) looks like this:

let initState = Map({ files: List(), displayMode: 'grid', currentRequests: List() }); 

I have a thunk method that starts a download and dispatches actions when an event occurs (for example, updating a run). For example, the onProgress event looks like this:

 onProgress: (data) => { dispatch(fileUploadProgressUpdated({ index, progress: data.percentage })); } 

I use redux-actions to create and process my actions, so my reducer for this 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 this:

 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, all this is working fine! In Redux DevTools, it appears as an action, as expected. However, none of my components are updated! Adding new elements to the files array re-displays my user interface with the new files added, so Redux, of course, has no problem with what I am doing this ...

My top-level component, which connects to the repository 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 .

An object

A file in the files array is basically like this:

 { name: '', progress: 0, status } 

UploadWidget is basically a div n drop div and a files array printed on the screen.

I tried using redux-immutablejs to help, as I saw in many posts on GitHub, but I have no idea if this 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 entry point to the application is as follows:

 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') ); 

Finally, 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 the β€œ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 changed objects no matter how deep you update them?

Getting Immutable to work with Redux doesn't seem as easy as it sounds: How to use Immutable.js with a shorthand? https://github.com/reactjs/redux/issues/548

However, the exaggerated benefits of using Immutable seem worthy of this battle, and I LOVE to understand what I'm doing wrong!

UPDATE April 10, 2016 The selected answer told me what I am doing wrong and for the sake of completeness, my updateFilePropsAtIndex function now simply contains the following:

 return state.updateIn(['files', index], file => Object.assign({}, file, fileProps) ); 

This works great! :)

+6
source share
1 answer

First, two general thoughts:

  • Immutable.js is potentially useful, yes, but you can perform the same immutable data processing without using it. There are several libraries that can help simplify reading immutable data, but still work with ordinary objects and arrays. Many of these are listed on the Immutable Data page in my Redux-related repositories.
  • If the React component is not updated, it is almost always because the reducer actually mutates the data. The Redux FAQ has an answer on this topic, http://redux.js.org/docs/FAQ.html#react-not-rerendering .

Now, given that you are using Immutable.js, I will admit that a data mutation seems unlikely. However ... the line file[prop] = fileProps[prop] in your reducer seems awfully curious. What exactly do you expect from there? I would take a good look at this part.

Actually, now when I look at this ... I'm almost 100% sure that you are mutating data. The updater callback for state.updateIn(['files', index]) returns the same file that you received as a parameter. Per the Immutable.js docs at https://facebook.imtqy.com/immutable-js/docs/#/Map :

If the updater function returns the same value with which it was called, then no changes will occur. This is still true if notSetValue is provided.

So yes. You return the same value that you were given, your direct mutations to it appear in DevTools, because this object is still hanging around, but since you returned the same object, Immutable.js does not actually return any modified objects beyond the hierarchy. Thus, when Redux checks the top-level object, it does not see anything that has changed, it does not notify subscribers, and therefore your mapStateToProps component never starts.

Clean the gearbox and return the new object from within this update, and everything should work.

(A rather belated answer, but I just saw this question now, and it seems to be still open. I hope you actually fixed it already ...)

+5
source

All Articles