React容器组件范式实践疑问:路由、拆分及优势示例
针对你在开发App时遇到的容器组件相关疑问,结合你的代码示例,我来逐一解答:
1. 路由的component属性是否必须传入容器组件?
当然不是必须的!路由组件可以是哑组件,也可以是容器组件,完全取决于这个组件是否需要直接和Redux store交互(比如获取状态、触发action):
- 如果你的哑组件不需要从store拿数据,也不需要触发action,只是接收父组件传的props来渲染,那直接把它放在路由里完全没问题。比如你的
AddProductComponent现在只是展示UI,暂时没有数据逻辑,路由里直接用它完全ok。 - 但如果这个组件需要访问Redux状态或者触发action,那你有两种选择:
- 把它包裹在对应的容器组件里,路由传入容器组件(比如你现在的
ProductContainer); - 用
connect直接把这个哑组件连接到store,让它变成一个容器组件(不过这种方式会模糊哑组件的职责,不太推荐)。
- 把它包裹在对应的容器组件里,路由传入容器组件(比如你现在的
举个例子,你现在路由里的<Route exact path="/products" component={ListProductComponent} />,如果ListProductComponent需要从store获取products数据,那最好把它包在一个ListProductContainer里,路由指向这个容器,而不是直接用哑组件。
2. 是否需要为列表、添加产品分别创建独立容器?
这不是硬性规定,但拆分独立容器会让你的代码更符合单一职责原则,后续维护和复用会更方便:
你现在用一个ProductContainer处理所有CRUD,虽然能运行,但如果要单独展示AddProductComponent(比如用户进入/products/:productId只想添加/编辑产品),或者单独展示ListProductComponent(进入/products只看列表),当前的ProductContainer会同时渲染两个哑组件,显然不符合需求。
拆分方案示例:
- 创建
ListProductContainer:只负责获取产品列表的逻辑,渲染ListProductComponent
class ListProductContainer extends React.Component { constructor(props) { super(props); this.fetchProducts = this.fetchProducts.bind(this); } fetchProducts = () => { this.props.dispatch(fetchProductsBegin()); getSitesApi.getAll(1) .then(response => { if (response.data) { this.props.dispatch(fetchProductsSuccess(response.data._embedded.companies)); } else { this.props.dispatch(fetchProductsFailure({message: "Fetching products failed"})); } }); }; componentDidMount() { this.fetchProducts(); } render() { const {products} = this.props; return ( <div className="ListProductContainer"> <Button color="danger" onClick={this.fetchProducts}>Load Products</Button> <ListProductComponent products={products}/> </div> ); } } const mapStateToProps = state => ({ products: state.products }); export default connect(mapStateToProps)(ListProductContainer);
- 创建
AddProductContainer:负责产品添加/编辑的逻辑(比如表单提交、调用添加接口),渲染AddProductComponent
class AddProductContainer extends React.Component { addProduct = () => { // 这里实现添加产品的逻辑,调用API、触发action等 // 比如:this.props.dispatch(addProduct(newProduct)); }; render() { return ( <div className="AddProductContainer"> <AddProductComponent onAddProduct={this.addProduct}/> </div> ); } } export default connect()(AddProductContainer);
然后路由就可以这样配置:
<Router> <Switch> <Route exact path="/" component={AppComponent}/> <Route exact path="/products" component={ListProductContainer} /> <Route path="/products/:productId" component={AddProductContainer} /> </Switch> </Router>
这样每个容器只负责自己的逻辑,路由也能精准渲染对应的组件,完美解决你“只想展示某一个哑组件”的需求。
3. 容器组件的核心目标是什么?为什么选它而非常规方式?
容器组件的核心目标就是分离数据逻辑和UI渲染,咱们对比常规方式(把所有逻辑写在一个组件里)就能看出优势:
常规方式的问题:
如果把fetchProducts、状态管理、UI渲染都写在ListProductComponent里,会导致:
- 组件臃肿,既有数据请求、action触发,又有列表渲染,维护起来头疼;
- UI部分无法复用,比如你想在另一个页面展示同样的产品列表,必须把整个组件(包括数据逻辑)搬过去;
- 测试困难,测试UI的时候还要模拟数据请求,测试逻辑的时候还要处理UI渲染。
容器组件方案的优势:
- 职责清晰:容器组件只管“数据怎么来、怎么处理”,哑组件只管“数据怎么展示”;
- 复用性高:比如你的
ListProductComponent可以在任何需要展示产品列表的地方复用,只要给它传不同的productsprops就行; - 测试简单:哑组件可以单独做UI测试(比如快照测试),容器组件单独测试数据逻辑(比如模拟API请求、验证action触发);
- 代码更易维护:后续要修改列表的UI,直接改
ListProductComponent;要修改获取数据的逻辑,直接改ListProductContainer,互不影响。
比如你的代码里,ListProductComponent只负责渲染列表和加载状态,不管数据是从哪里来的——哪怕以后你不用Redux了,改用Context或者其他状态管理,只要给它传对products props,它就能正常工作,这就是分离带来的灵活性。
内容的提问来源于stack exchange,提问作者Mizlul

