Phoenix LiveComponent Provider Pattern

Published: (January 31, 2026 at 11:46 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

Cover image for Phoenix LiveComponent Provider Pattern

Mykolas Mankevicius

If you’ve worked with React, you’ve probably used the Provider pattern. It’s a component that wraps children and provides them with data via context.

In Phoenix LiveView, we can achieve something similar using LiveComponent’s :let directive combined with assign_async.

The Problem

I had a product‑detail page that needed to show similar products. The naive approach would be to load them in the parent LiveView’s 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

Two problems here:

  • Blocking mount – the search delays the entire page render.
  • Scattered concerns – similar‑products logic lives in the parent LiveView.

The Provider Pattern

Instead, I created a provider component that handles its own data loading and exposes the result to its children via :let.

The 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

The key line is:

{render_slot(@inner_block, %{items: @search.result.hits, query: @search.result.query})}

By passing a map, you control exactly what the provider exposes. Need to add more data later? Just add another key.

The Component Module

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

Usage

For most cases, you don’t even need to know about the provider:

# Grid layout – just use it

Custom loading/empty states – still simple:

# (Insert custom loading / empty state markup here)

Custom rendering? Reach for the 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>

Why This Works

  • Simple things are simple – the default grid works out‑of‑the‑box.
  • Complex things are possible – the provider exposes full visual control when needed.
  • Self‑contained async – the provider owns its data fetching.
  • Extensible – need more data? Add a key to the context map.

This is essentially React’s Context Provider pattern, but in Phoenix LiveView.

LiveView

The product detail page went from managing similar‑products loading to simply declaring where they should appear. Much cleaner.

This post was written with the help of AI, but I curate and review everything thoroughly. The ideas, code, and opinions are all mine.

Back to Blog

Related posts

Read more »

CSS Selectors 101

CSS Selectors – How to Target Elements You've written some HTML. Now you want to make it look good with CSS. But how do you tell CSS which elements to style? T...

A Portfolio You Query, Not Scroll

Introduction Most developer portfolios create friction. Under time pressure, reviewers miss important details—not because they are absent, but because they are...