06-高级模式与实战项目——05. 容器与展示组件

发布时间:2026/7/7 1:58:20

06-高级模式与实战项目——05. 容器与展示组件 05. 容器与展示组件概述容器与展示组件模式是一种将逻辑与 UI 分离的设计模式。容器组件负责数据获取和状态管理展示组件负责 UI 渲染。这种模式提高了代码的可测试性和复用性。维度内容What容器组件负责逻辑和数据展示组件负责 UI 渲染Why分离关注点提高可测试性和复用性When组件有复杂逻辑和 UI 时Where组件分层设计Who需要组件逻辑复用的开发者HowContainerComponent→ 获取数据 →PresentationalComponent渲染1. 什么是容器与展示组件1.1 基本概念容器组件负责数据获取、状态管理、业务逻辑展示组件负责 UI 渲染通过 props 接收数据// 展示组件只负责 UI 渲染 function UserList({ users, loading, error, onRetry }) { if (loading) return div加载中.../div; if (error) return div错误: {error.message} button onClick{onRetry}重试/button/div; return ( ul {users.map(user ( li key{user.id}{user.name}/li ))} /ul ); } // 容器组件负责数据获取和逻辑 function UserListContainer() { const [users, setUsers] useState([]); const [loading, setLoading] useState(true); const [error, setError] useState(null); const fetchUsers async () { try { setLoading(true); const response await fetch(/api/users); const data await response.json(); setUsers(data); } catch (err) { setError(err); } finally { setLoading(false); } }; useEffect(() { fetchUsers(); }, []); return ( UserList users{users} loading{loading} error{error} onRetry{fetchUsers} / ); }1.2 为什么需要分离// ❌ 耦合在一起难以测试和复用 function UserList() { const [users, setUsers] useState([]); const [loading, setLoading] useState(true); useEffect(() { fetch(/api/users) .then(res res.json()) .then(setUsers) .finally(() setLoading(false)); }, []); if (loading) return div加载中.../div; return ul{users.map(user li key{user.id}{user.name}/li)}/ul; }2. 展示组件2.1 展示组件的特点只关心 UI 渲染通过 props 接收数据不依赖外部 API易于测试易于复用// 展示组件示例 function Button({ children, onClick, disabled, variant primary }) { const variants { primary: bg-blue-500 text-white, secondary: bg-gray-500 text-white, danger: bg-red-500 text-white, }; return ( button onClick{onClick} disabled{disabled} className{px-4 py-2 rounded ${variants[variant]} ${disabled ? opacity-50 : }} {children} /button ); } function Card({ title, children, footer }) { return ( div classNameborder rounded-lg shadow {title div classNameborder-b p-4 font-bold{title}/div} div classNamep-4{children}/div {footer div classNameborder-t p-4 text-gray-500{footer}/div} /div ); } function UserCard({ user, onEdit, onDelete }) { return ( Card title{user.name} footer{邮箱: ${user.email}} p角色: {user.role}/p div classNameflex gap-2 mt-4 Button variantsecondary onClick{() onEdit(user)}编辑/Button Button variantdanger onClick{() onDelete(user.id)}删除/Button /div /Card ); }2.2 展示组件的测试// 展示组件测试非常容易 import { render, screen, fireEvent } from testing-library/react; test(UserCard 渲染用户信息, () { const user { id: 1, name: 张三, email: zhangexample.com, role: admin }; const onEdit jest.fn(); const onDelete jest.fn(); render(UserCard user{user} onEdit{onEdit} onDelete{onDelete} /); expect(screen.getByText(张三)).toBeInTheDocument(); expect(screen.getByText(zhangexample.com)).toBeInTheDocument(); fireEvent.click(screen.getByText(编辑)); expect(onEdit).toHaveBeenCalledWith(user); });3. 容器组件3.1 容器组件的特点负责数据获取负责状态管理负责业务逻辑将数据传递给展示组件// 容器组件示例 function UserListContainer() { const [users, setUsers] useState([]); const [loading, setLoading] useState(true); const [error, setError] useState(null); const fetchUsers useCallback(async () { try { setLoading(true); const response await api.getUsers(); setUsers(response.data); } catch (err) { setError(err); } finally { setLoading(false); } }, []); const handleEdit useCallback(async (user) { await api.updateUser(user.id, user); fetchUsers(); // 刷新列表 }, [fetchUsers]); const handleDelete useCallback(async (id) { await api.deleteUser(id); fetchUsers(); }, [fetchUsers]); useEffect(() { fetchUsers(); }, [fetchUsers]); return ( div h1用户管理/h1 {loading ? ( Spinner / ) : error ? ( ErrorMessage error{error} onRetry{fetchUsers} / ) : ( div classNameuser-grid {users.map(user ( UserCard key{user.id} user{user} onEdit{handleEdit} onDelete{handleDelete} / ))} /div )} /div ); }3.2 容器组件的测试// 容器组件测试需要 mock API import { render, screen, waitFor } from testing-library/react; import { api } from ./api; jest.mock(./api); test(UserListContainer 加载并显示用户, async () { const mockUsers [ { id: 1, name: 张三, email: zhangexample.com, role: admin }, ]; api.getUsers.mockResolvedValue({ data: mockUsers }); render(UserListContainer /); expect(screen.getByText(加载中...)).toBeInTheDocument(); await waitFor(() { expect(screen.getByText(张三)).toBeInTheDocument(); }); });4. 实际应用场景4.1 数据获取场景// 展示组件 function PostList({ posts, loading, error, onRetry }) { if (loading) return PostSkeleton count{5} /; if (error) return ErrorView error{error} onRetry{onRetry} /; return ( div classNameposts {posts.map(post ( PostCard key{post.id} post{post} / ))} /div ); } // 容器组件 function PostListContainer() { const { data: posts, isLoading, error, refetch } useQuery({ queryKey: [posts], queryFn: () fetch(/api/posts).then(res res.json()), }); return ( PostList posts{posts} loading{isLoading} error{error} onRetry{refetch} / ); }4.2 表单场景// 展示组件表单 UI function LoginForm({ values, errors, onChange, onSubmit, isLoading }) { return ( form onSubmit{onSubmit} classNamelogin-form div input nameemail typeemail value{values.email} onChange{onChange} placeholder邮箱 className{errors.email ? error : } / {errors.email span classNameerror{errors.email}/span} /div div input namepassword typepassword value{values.password} onChange{onChange} placeholder密码 / /div button typesubmit disabled{isLoading} {isLoading ? 登录中... : 登录} /button /form ); } // 容器组件表单逻辑 function LoginFormContainer() { const [values, setValues] useState({ email: , password: }); const [errors, setErrors] useState({}); const [isLoading, setIsLoading] useState(false); const handleChange (e) { const { name, value } e.target; setValues(prev ({ ...prev, [name]: value })); // 实时验证 if (name email !value.includes()) { setErrors(prev ({ ...prev, email: 邮箱格式错误 })); } else { setErrors(prev ({ ...prev, email: })); } }; const handleSubmit async (e) { e.preventDefault(); setIsLoading(true); try { await login(values.email, values.password); // 跳转到首页 } catch (error) { setErrors(prev ({ ...prev, form: error.message })); } finally { setIsLoading(false); } }; return ( LoginForm values{values} errors{errors} onChange{handleChange} onSubmit{handleSubmit} isLoading{isLoading} / ); }5. 完整示例商品管理系统// 展示组件 // ProductCard 展示组件 function ProductCard({ product, onEdit, onDelete }) { return ( div classNameproduct-card img src{product.image} alt{product.name} / h3{product.name}/h3 p classNameprice¥{product.price}/p p classNamestock库存: {product.stock}/p div classNameactions button onClick{() onEdit(product)}编辑/button button onClick{() onDelete(product.id)}删除/button /div /div ); } // ProductForm 展示组件 function ProductForm({ initialValues, onSubmit, isLoading }) { const [values, setValues] useState(initialValues || { name: , price: , stock: }); const handleSubmit (e) { e.preventDefault(); onSubmit(values); }; return ( form onSubmit{handleSubmit} classNameproduct-form input value{values.name} onChange{(e) setValues({ ...values, name: e.target.value })} placeholder商品名称 required / input typenumber value{values.price} onChange{(e) setValues({ ...values, price: e.target.value })} placeholder价格 required / input typenumber value{values.stock} onChange{(e) setValues({ ...values, stock: e.target.value })} placeholder库存 required / button typesubmit disabled{isLoading} {isLoading ? 保存中... : 保存} /button /form ); } // ProductList 展示组件 function ProductList({ products, loading, error, onRetry, onEdit, onDelete }) { if (loading) return div加载商品中.../div; if (error) return ( div 错误: {error.message} button onClick{onRetry}重试/button /div ); return ( div classNameproduct-grid {products.map(product ( ProductCard key{product.id} product{product} onEdit{onEdit} onDelete{onDelete} / ))} /div ); } // 容器组件 function ProductManagement() { const [products, setProducts] useState([]); const [loading, setLoading] useState(true); const [error, setError] useState(null); const [showForm, setShowForm] useState(false); const [editingProduct, setEditingProduct] useState(null); const [saving, setSaving] useState(false); const fetchProducts useCallback(async () { try { setLoading(true); const response await fetch(/api/products); const data await response.json(); setProducts(data); } catch (err) { setError(err); } finally { setLoading(false); } }, []); const handleSave async (product) { setSaving(true); try { const url editingProduct ? /api/products/${editingProduct.id} : /api/products; const method editingProduct ? PUT : POST; await fetch(url, { method, headers: { Content-Type: application/json }, body: JSON.stringify(product), }); await fetchProducts(); setShowForm(false); setEditingProduct(null); } catch (err) { console.error(保存失败:, err); } finally { setSaving(false); } }; const handleEdit (product) { setEditingProduct(product); setShowForm(true); }; const handleDelete async (id) { if (!confirm(确定删除吗)) return; try { await fetch(/api/products/${id}, { method: DELETE }); await fetchProducts(); } catch (err) { console.error(删除失败:, err); } }; useEffect(() { fetchProducts(); }, [fetchProducts]); return ( div classNameproduct-management div classNameheader h1商品管理/h1 button onClick{() setShowForm(true)}添加商品/button /div {showForm ( div classNamemodal div classNamemodal-content h2{editingProduct ? 编辑商品 : 添加商品}/h2 ProductForm initialValues{editingProduct} onSubmit{handleSave} isLoading{saving} / button onClick{() { setShowForm(false); setEditingProduct(null); }}取消/button /div /div )} ProductList products{products} loading{loading} error{error} onRetry{fetchProducts} onEdit{handleEdit} onDelete{handleDelete} / /div ); }6. 总结核心要点类型职责特点展示组件UI 渲染纯函数、易测试、易复用容器组件逻辑状态有副作用、连接数据优势关注点分离提高可测试性提高可复用性便于协作开发记忆口诀容器组件管逻辑展示组件管 UI数据 props 传下去测试复用都容易7. 相关资源Presentational and Container ComponentsReact 组件模式

相关新闻