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>
)
};
这是我所拥有内容的简化表示:
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>
)
};