Phoenix LiveComponent 提供者模式

发布: (2026年2月1日 GMT+8 00:46)
4 min read
原文: Dev.to

Source: Dev.to

Cover image for Phoenix LiveComponent Provider Pattern

Mykolas Mankevicius

如果你使用过 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 的帮助下撰写,但我对所有内容进行精心策划和审阅。所有想法、代码和观点均为本人所有。

Back to Blog

相关文章

阅读更多 »

CSS 选择器 101

CSS 选择器 – 如何定位元素 您已经编写了一些 HTML。现在您想用 CSS 让它看起来更好。但您如何告诉 CSS 哪些元素需要样式?T...