高级 React 模式:渲染属性
嗨👋
如果你使用 React 已经有一段时间了,那么你一定很擅长编写可复用组件。可复用的 UI 组件!但是随着代码库规模的扩大,你经常会遇到这样的情况:你想共享业务逻辑,但 UI 部分可能有所不同。
💡 这种情况正是运用一些高级模式的绝佳时机。渲染属性(Render Props)就是这样一种模式。
🚀 使用渲染属性模式的一些库包括 React Router、Downshift 和 Formik。
我们先来看一个例子。
例如,您正在搭建一个展示产品的在线商店。
每个产品列表都需要遵循一些通用的业务逻辑:
- ✈️ 点击后跳转至产品页面
- 🤑 按价格对产品进行排序
- 💾 节省产品
- 如果用户未登录,则保存到本地存储。
- 否则,使用 API 调用将其存储在数据库中。
🎨 但根据列表的渲染位置不同,用户界面也会有所不同。有的地方可能想显示统计数据或产品图片,有的地方可能只想显示标题。
🧠 首先,让我们了解一下渲染属性组件的基本结构。
const Wrapper = ({ products, render }) => {
// do some stuff
const updatedProducts = someOperations(products)
// provide some helper funcs for data like sort func
const sort = () => {}
return render({ updatedProducts, sort })
}
👉 Render Props 组件只是对 UI 组件的封装。上面的组件接收两个 props:`data`products和render`function`。`data`products是需要使用业务逻辑修改的数据,` renderfunction` 是一个函数,它会接收修改后的数据和其他一些辅助函数。
🤔 但是我该如何使用这个组件呢?
// import everything
const HomeScreenProducts = () => {
// assume you have this hook
const { products } = useData()
return (
<ProductsWrapper
products={products}
render={
({ updatedProducts, sort }) => updatedProducts.map(product => <ProductCard />)
}
/>
)
}
👉 我们的HomeScreenProducts组件使用ProductsWrapper并处理所有业务逻辑。用户界面仍然由调用组件控制。在函数
内部,render我们使用修改后的产品数据并据此渲染用户界面。
😰 这看起来有点复杂。但我们可以简化它,使其 API 更简洁。与其render单独传递函数,不如直接使用childrenprop。
更新后,两个组件看起来是这样的。
const Wrapper = ({ products, children }) => {
// same stuff here
return children({ updatedProducts, sort })
}
const HomeScreenProducts = () => {
// same stuff here
return (
<ProductsWrapper products={products}>
{({ updatedProducts, sort }) => updatedProducts.map(product => <ProductCard />}
</ProductsWrapper>
)
}
👌 这样好多了。这个children道具和我们之前用的道具功能相同render。这种编写渲染道具的方式更常见。
⚠️ 别忘了添加key到你的清单中。
💪 现在我们已经了解了渲染属性模式,我们可以完成前面提到的任务了。
import React from "react";
import { useAuth } from "./hooks/useAuth";
const ProductsWrapper = ({ products, children }) => {
const { isLoggedIn } = useAuth();
const [sortedProducts, setSortedProducts] = React.useState(products);
const sort = (order) => {
const reorderedProducts = [...products];
reorderedProducts.sort((a, b) => {
if (order === "desc") {
return b.price > a.price;
} else {
return a.price > b.price;
}
});
setSortedProducts(reorderedProducts);
};
const save = (productId) => {
if (isLoggedIn) {
// call API
console.log("Saving in DB... ", productId);
} else {
// save to local storage
console.log("Saving in local storage... ", productId);
}
};
const navigate = () => {
console.log("Navigating...");
};
return children({ sortedProducts, sort, save, navigate });
};
export default ProductsWrapper;
我们扩展了ProductsWrapper组件,并为其添加了所有必需的功能。它children以函数的形式调用,并传递数据和辅助函数。
import ProductsWrapper from "./ProductsWrapper";
const products = [
{ id: 1, name: "Coffee", price: 2 },
{ id: 2, name: "Choclates", price: 3 },
{ id: 3, name: "Milk", price: 5 },
{ id: 4, name: "Eggs", price: 4 },
{ id: 5, name: "Bread", price: 1 }
];
export default function App() {
return (
<div className="App">
<ProductsWrapper products={products}>
{({ sortedProducts, sort, save, navigate }) => (
<>
<div className="flex">
<button onClick={() => sort("desc")}>Price: High to Low</button>
<button onClick={() => sort("asc")}>Price: Low to High</button>
</div>
{sortedProducts.map((product) => (
<div className="product" key={product.id}>
<span onClick={() => navigate(product.id)}>
{product.name} - ${product.price}
</span>
<button className="save-btn" onClick={() => save(product.id)}>
save
</button>
</div>
))}
</>
)}
</ProductsWrapper>
</div>
);
}
✅ 我们的 UI 组件负责ProductsWrapper用户界面的设计和维护。如您所见,我们可以自由修改用户界面,或创建与此完全不同的其他 UI 组件。我们的业务逻辑集中在一处。
如果您想尝试一下这个示例,可以在 CodeSandbox 上找到:https://codesandbox.io/s/render-props-example-6190fb
就这些啦!👋
🤙 如果这篇文章对您有帮助,请考虑分享,也可以在LinkedIn和Twitter上与我联系。
文章来源:https://dev.to/thesanjeevsharma/advance-react-patterns-render-props-3hjj