Phoenix LiveComponent 提供者模式
Source: Dev.to

如果你使用过 React,可能已经使用过 Provider 模式。它是一个包装子组件并通过 context 向它们提供数据的组件。
在 Phoenix LiveView 中,我们可以使用 LiveComponent 的 :let 指令结合 assign_async 来实现类似的功能。
Source:
问题
我有一个产品详情页,需要展示相似产品。最直接的做法是把它们放在父 LiveView 的 mount 中加载:
def mount(params, _session, socket) do
product = get_product!(params)
# Blocking the mount with an async search operation 🙈
{:ok, similar_products} = Search.similar_products(product.id, limit: 12)
socket
|> assign(:product, product)
|> assign(:similar_products, similar_products)
|> ok()
end
这里有两个问题:
- 阻塞 mount – 搜索会延迟整个页面的渲染。
- 关注点分散 – 相似产品的逻辑位于父 LiveView 中。
Provider 模式
相反,我创建了一个 provider 组件,它自行处理数据加载,并通过 :let 将结果暴露给其子组件。
Provider(LiveComponent)
defmodule MarkoWeb.Components.SimilarProducts.Provider do
use MarkoWeb, :live_component
@impl LiveComponent
def update(assigns, socket) do
{%{product_id: product_id, limit: limit}, assigns} =
Map.split(assigns, [:product_id, :limit])
socket
|> assign(assigns)
|> assign_async(:search, fn ->
{:ok, result} = Search.similar_products(product_id, limit: limit)
{:ok, %{search: result}}
end)
|> ok()
end
@impl LiveComponent
def render(assigns) do
~H"""
{render_slot(@loading)}
{render_slot(@inner_block, %{items: @search.result.hits, query: @search.result.query})}
{render_slot(@no_results)}
"""
end
defp loading(assigns) do
~H"""
"""
end
defp no_results(assigns) do
~H"""
No results
"""
end
end
关键行是:
{render_slot(@inner_block, %{items: @search.result.hits, query: @search.result.query})}
通过传递映射,你可以精确控制 provider 暴露的内容。以后需要添加更多数据?只需再添加一个键即可。
组件模块
defmodule MarkoWeb.Components.SimilarProducts do
use MarkoWeb, :component
# === Convenience components ===
attr :product, Product, required: true
attr :limit, :integer, required: true
slot :loading
slot :no_results
def grid(assigns) do
~H"""
{render_slot(slot)}
{render_slot(slot)}
"""
end
# === Provider (for custom rendering) ===
attr :id, :any, default: nil
attr :product, Product, required: true
attr :limit, :integer, required: true
slot :inner_block, required: true
slot :loading
slot :no_results
def provider(assigns) do
~H"""
{render_slot(@inner_block, context)}
"""
end
# === Inner presentation ===
defp grid_inner(assigns) do
~H"""
-
"""
end
end
用法
对于大多数情况,您甚至不需要了解 provider:
# Grid layout – just use it
自定义加载/空状态 – 仍然简单:
# (Insert custom loading / empty state markup here)
自定义渲染?请使用 provider:
# (Insert custom rendering markup here)
# Example call inside a template
<.similar_products.provider
product={@product}
limit={12}
loading={...}
no_results={...}>
<:inner_block let={%{items: items, query: query}}>
<!-- custom rendering using items and query -->
</:inner_block>
</.similar_products.provider>
为什么这样有效
- 简单的事物就是简单 – 默认网格开箱即用。
- 复杂的事物是可能的 – 当需要时,提供者暴露完整的视觉控制。
- 自包含的异步 – 提供者拥有自己的数据获取。
- 可扩展 – 需要更多数据?向上下文映射添加一个键。
这本质上是 React 的 Context Provider 模式,但在 Phoenix LiveView 中实现。
LiveView
产品详情页从管理相似产品的加载,变为仅声明它们应出现的位置。更加简洁。
本文在 AI 的帮助下撰写,但我对所有内容进行精心策划和审阅。所有想法、代码和观点均为本人所有。
