Redux not updating components when deep Immutable

2019-04-27 01:10发布

问题:

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! :)

回答1:

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...)