Jake Prem's Blog

Easy Phoenix Playground Restarts in LiveBook

Published March 2, 2025

The Problem

Phoenix Playground is a great tool for quickly demonstrating and testing small bits of Phoenix Code. LiveBook is a great tool for sharing and easily running Elixir code from the browser. But when you run a playground in LiveBook there’s no easy way to stop it and start another one with different code.

Our goal in this post is to come up with a simple solution that will let us easily run multiple Phoenix examples in a row in a single LiveBook.

The Solution

PlaygroundManager, a super simple module with a helper function to stop the current playground if it’s running, and a helper function that will automatically stop the current playground and start a new one with the given options.

defmodule PlaygroundManager do
  @moduledoc """
  A wrapper around PhoenixPlayground that makes it more LiveBook friendly.
  """

  def start(opts) do
    stop()

    PhoenixPlayground.start(opts)
  end

  def stop do
    case Supervisor.which_children(PhoenixPlayground.Application) do
      [{PhoenixPlayground, pid, _, _} | _] when is_pid(pid) ->
        Supervisor.terminate_child(PhoenixPlayground.Application, PhoenixPlayground)
        Supervisor.delete_child(PhoenixPlayground.Application, PhoenixPlayground)
        :ok
      _ -> {:error, :not_running}
    end
  end
end

Now let’s try it out with some LiveViews.

defmodule DemoLive do
  use Phoenix.LiveView
  
  def render(assigns) do
    ~H"""
    <div>Welcome to DemoLive!!!</div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok, socket}
  end
end

PlaygroundManager.start(live: DemoLive)

Executing that block of code should start the playground and automatically open a new tab showing the DemoLiveView.

Now let’s define another simple counter LiveView and try launching that with PlaygroundManager.

defmodule CounterLive do
  use Phoenix.LiveView
  
  def render(assigns) do
    ~H"""
    <div>
      <button phx-click="update-count" phx-value-count={@count - 1}>-</button>
      <div>The current count is: <%= @count %></div>
      <button phx-click="update-count" phx-value-count={@count + 1}>+</button>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok, assign(socket, count: 0)}
  end

  def handle_event("update-count", %{"count" => count}, socket) do
    {:noreply, assign(socket, count: String.to_integer(count))}
  end
end

PlaygroundManager.start(live: CounterLive)

Running that block should launch a new tab showing the counter LiveView.

Putting them together

Both of our LiveView modules are still defined, which means we can easily use them again in a new example. For example, let’s define a simple Router that will let us visit both.

defmodule DemoRouter do
  use Phoenix.Router
  import Phoenix.LiveView.Router
  
  pipeline :browser do
    plug :accepts, ["html"]
  end

  scope "/" do
    pipe_through :browser

    live "/counter", CounterLive
    live "/", DemoLive
  end
end

PlaygroundManager.start(plug: DemoRouter)

Now you can visit the counter LiveView at /counter and the DemoLiveView at /.

Conclusion

This super simple module makes it much easier to demonstrate multiple Phoenix concepts in a single LiveBook.

You can use this immediately by simply including the module in the setup of your LiveBook, you could even forgo the module and just define anonymous functions but I think the module is probably nicer. Hopefully in the future some of this functionality will be included in Phoenix Playground itself.

Suggestions

  • Set a port variable early in the Livebook to avoid conflicts with other Phoenix apps that the reader of your LiveBook might have running.
  • Set an open_browser? variable early in the Livebook to let readers opt out of launching a new tab for each example.