React/Redux:分离展示组件和容器组件的困境。

React/Redux: separating presentational and container components dilemma.

这是我所拥有内容的简化表示:

container.js:

import Presentation from './Presentation';
import { connect } from 'react-redux';

export default connect(
    ({ someState}) => ({ ...someState }),
    (dispatch) => {
        onClick(e) => ({})
    }
)(Presentation);

pesentation.js:

export default ({ items, onClick}) => (
    <table>
        <tbody>
            {items.map((item, index) => (
                <tr>
                    <td onClick={onClick}>{item.a}</td>
                    <td>
                        <div onClick={onClick}>{item.b}</div>
                    </td>
                </tr>
            ))}
        </tbody>
    </table>
);

比方说,我想访问处理程序中每一行的 index 变量。 想到的第一个解决方案是使用闭包来捕获 index:

container.js:

onClick(e, index) => ({});

presentation.js:

<td onClick={e => onClick(e, index)}>{item.a}</td>
<td>
    <div onClick={e => onClick(e, index)}>{item.b}</div>
</td>

可行,但看起来很脏,所以我决定尽可能找到其他解决方案。 接下来是在每个元素上使用 data-index 属性:

container.js:

onClick({ target: { dataset: { index } } }) => ({});

presentation.js:

<td data-index={index} onClick={onClick}>{item.a}</td>
<td>
    <div data-index={index} onClick={onClick}>{item.b}</div>
</td>

也许更好,但是需要在多个元素上使用具有相同值的数据属性看起来是多余的。如果我有 20 个元素怎么办? 好的,没问题!让我们把它移到父容器:

container.js:

onClick(e) => {
    const {index} = e.target.closest('tr').dataset;
};

presentation.js:

<tr data-index={index}>
    <td onClick={onClick}><td>
    <td>
        <div onClick={onClick}>{item.b}</div>
    </td>
</tr>

好了,下一个问题就解决了。可是等等。这样,容器严重依赖于展示组件布局。通过引入对 tr 标记的引用,它与视图耦合。如果在某个时候标记将更改为 div 怎么办? 我想出的最终解决方案是为容器标签元素添加一个 CSS class,该元素从容器组件传递到表示:

container.js:

import Presentation from './Presentation';
import { connect } from 'react-redux';

const ITEM_CONTAINER_CLASS = 'item-cotainer';

export default connect(
    ({ someState }) => ({ ...someState, ITEM_CONTAINER_CLASS }),
    (dispatch) => {
        onClick(e) => {
            const {index} = e.target.closest(`.${ITEM_CONTAINER_CLASS }`).dataset;
        }
    }
)(Presentation);

pesentation.js:

export default ({ items, onClick, ITEM_CONTAINER_CLASS  }) => (
    <table>
        <tbody>
            {items.map((item, index) => (
                <tr className={ITEM_CONTAINER_CLASS}>
                    <td onClick={onClick}>{item.a}</td>
                    <td>
                        <div onClick={onClick}>{item.b}</div>
                    </td>
                </tr>
            ))}
        </tbody>
    </table>
);

但是,它仍然有一个小缺点:它需要 DOM 操作才能获取索引。

感谢阅读(如果有人)。请问还有其他选择吗?

这是经典的React/Redux困境。我的答案是将您的 "item view" 提取到一个组件并向其添加一个 onItemClick 道具。当然,您的项目视图应该收到要显示的 item,并且能够将其作为参数发送到您的容器。

e 参数属于您的演示文稿,而不属于作为业务逻辑的动作创建者。并将数据存储在 DOM 中(例如 React 根本不需要使用 data- 属性)。

像这样(我倾向于从下到上,从简单到复杂):

//Item.js
export default class Item extends Component {
   constructor(props){
     super(props)
     this.handleClick = this.handleClick.bind(this)
   }
   handleClick(e){
     e.preventDefault() //this e stays here
     this.props.onItemClick(this.props.item)
   }
   render(){
      const { item } = this.props
      return (
        <tr className={ITEM_CONTAINER_CLASS}>
          <td onClick={this.handleClick}>{item.a}</td>
          <td onClick={this.handleClick}>{item.b}</td>
        </tr>
      )
   }
}

// ItemList (Presentation)
export default ItemList = ({ items, onClick}) => (
  <table>
    <tbody>
      {
        items.map((item, index) => 
          <Item item={item} onItemClick={ onClick } />
        )
      }
    </tbody>
  </table>
)

// Container.js - just change your mapDispatchToProps to accept the full item, or whatever you're interested in (perhaps only the id)
import ItemList from './ItemList';
import { connect } from 'react-redux';

export default connect(
    ({ someState}) => ({ ...someState }),
    {
       onClick(item){  
         return yourActionCreatorWith(item.id)
       }
    }
)(Presentation);

这样您就可以在需要派送时获得所需的一切。此外,将 "item view" 创建为单独的组件将使您能够单独测试它,甚至在需要时重用它。

看起来工作量更大,但相信我,这是值得的。

另请参阅已接受的答案。 解决方法如下:

container.js

import Presentation from './presentation';
import { connect } from 'react-redux';

export default connect(
    ({ someState }) => ({ ...someState }),
    (dispatch) => {
        onClick(index) => {}
    }
)(Presentation);

presentation.js

import Item from './item.js';

export default ({ items, onClick }) => (
    <table>
        <tbody>
            {items.map((item, index) => <Item index={index} item={item} onClick={onClick} />)}
        </tbody>
    </table>
);

item.js

export default ({ index, item, onClick }) => {
    const onItemClick = () => onClick(index);
    return (
        <tr>
            <td onClick={onItemClick}>{item.a}</td>
            <td>
                <div onClick={onItemClick}>{item.b}</div>
            </td>
        </tr>
    )
};