最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Redux not updating components when deep Immutable state properties are updated - Stack Overflow

programmeradmin4浏览0评论

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 ponent?

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 ponents 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 ponent 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 { bineReducers } from 'redux-immutablejs';
import { routeReducer as router } from 'redux-simple-router';
import UploaderReducer from './modules/UploaderReducer';

export default bineReducers({
  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/> ponent 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 ponents:

 {
    UploaderReducer: {
        files: [{progress: 0}]
    }
 }

Why is this? I thought the whole idea of using Immutable.js was that it was easier to pare 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?

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 pleteness, my updateFilePropsAtIndex function now contains simply this:

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

This works perfectly well! :)

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 ponent?

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 ponents 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 ponent 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 { bineReducers } from 'redux-immutablejs';
import { routeReducer as router } from 'redux-simple-router';
import UploaderReducer from './modules/UploaderReducer';

export default bineReducers({
  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/> ponent 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 ponents:

 {
    UploaderReducer: {
        files: [{progress: 0}]
    }
 }

Why is this? I thought the whole idea of using Immutable.js was that it was easier to pare 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./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 pleteness, my updateFilePropsAtIndex function now contains simply this:

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

This works perfectly well! :)

Share Improve this question edited May 23, 2017 at 10:30 CommunityBot 11 silver badge asked Feb 27, 2016 at 23:52 Chris PatonChris Paton 5,2334 gold badges42 silver badges53 bronze badges 4
  • When you say "the ponent is not being updated" do you mean that it's not receiving new props from redux at all? Does mapStateToProps not receive the updated progress? How about the reducer, does the action have the correct progress in the payload? – azium Commented Feb 28, 2016 at 1:45
  • The UploaderContainer mapStateToProps function doesn't fire at all, despite seeing the state updated in DevTools with the expected values. I have unit tests checking that what I'm doing should work, so I feel like there is something I'm not understanding at the Redux level, but I don't know what it is! – Chris Paton Commented Feb 28, 2016 at 9:01
  • Can you get a working example online on JSBin ? It woud be easier to work this out. – yachaka Commented Apr 7, 2016 at 15:28
  • Are you mixing immutable maps and plain objects in your UploaderReducer files List? Is your updateFilePropsAtIndex function called when you dispatch the action ? – VonD Commented Apr 7, 2016 at 15:36
Add a ment  | 

1 Answer 1

Reset to default 6 +50

Two general thoughts first:

  • Immutable.js is potentially useful, yes, but you can acplish 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 ponent 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/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 ponent'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...)

发布评论

评论列表(0)

  1. 暂无评论