简体   繁体   中英

Merging Immutable.js Maps in Flux store with internal React component state

I have been slowly converting my React+Flux apps to use Immutable.js data structures. I use the original, vanilla FB implementation of Flux.

One problem I have encountered is mixing component state with state received from Flux stores .

I keep all important business-logic state in stores. But my rule has been to keep UI-related state within components. Stores don't need to concern themselves if, for example, a dropdown menu is open or not, right?

The problem comes when an action is taken in a component that changes state in that same component's store. Let's say we have a component with a dropdown menu that is open. An item is selected from that dropdown menu. The action propagates to the ItemStore , the store emits a change, and the component gets new state from the store.

_onChange() {
  this.setState(this._getState());
}
_getState() {
  if(this.state === undefined) {
    return {
      data: Immutable.Map({
        selectedItem: ItemStore.getSelectedItem(),
        items: ItemStore.getItems(),
        menuIsOpen: false
      })
    };
  }
  return {
    data: this.state.data.merge(Immutable.Map({
      selectedItem: ItemStore.getSelectedItem(),
      items: ItemStore.getItems(),
      menuIsOpen: this.state.data.get("menuIsOpen")
    }))
  };
}

Concurrently, in the component, the click on the dropdown menu item emits an old-fashioned onClick event. I have a _handleClick function which uses setState to close the dropdown menu (local state).

_handleClick(event) {
  event.preventDefault();
  this.setState({
    data: this.state.data.set("menuIsOpen", !this.state.data.get("menuIsOpen"))
  });
}

The problem is that _handleClick ends up being called so soon after _getState that it doesn't have an updated copy of this.state.data . So in the component's render method, this.state.data.get("selectedItem") still shows the previously-selected item.

When I do this with POJOs, React's setState seems to batch everything correctly, so it was never an issue. But I don't want to have state that is not part of an Immutable.Map, because I want to take advantage of "pure" render ing. Yet I don't want to introduce UI state into my stores, because I feel like that could get messy real quick.

Is there a way I could fix this? Or is it just a bad practice to merge local Immutable.Map state and Immutable.Map store state within a single component?

RELATED: I am not a fan of the boilerplate if(this.state === undefined) logic to set initial local menuIsOpen state in my _getState method. This may be a sign that I am trying to do something that is not correct.

You can pass a callback to setState to enqueue an atomic update.

_onChange() {
  this.setState(state => this._getState(state));
}
_getState(state) {
  if(state === undefined) {
    return {
      data: Immutable.Map({
        selectedItem: ItemStore.getSelectedItem(),
        items: ItemStore.getItems(),
        menuIsOpen: false
      })
    };
  }
  return {
    data: state.data.merge(Immutable.Map({
      selectedItem: ItemStore.getSelectedItem(),
      items: ItemStore.getItems(),
      menuIsOpen: state.data.get("menuIsOpen")
    }))
  };
}

About your related point, you might want to take a look at getInitialState .

Why have 2 separate actions occur when you click (fire action to store, close menu)? Instead, you could say, when they click a menu item, we need to close the menu item, and alert the store of the new value.

_handleClick(event) {
  event.preventDefault();
  this.setState({
    data: this.state.data.set("menuIsOpen", false)
  }, function() {
    alertTheSelectionChange(selectedItem)
  });
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM