Phoenix 1.7 is View-less! 😱

Phoenix 1.7-rc’s announcement mentions that Phoenix is dropping Phoenix.View.

But don’t worry. It’s for a good reason.

Phoenix team unified the HTML rendering approaches whether from a controller request, or a LiveView.

So, controllers and LiveViews will both render function components!

Here’s how it works. 👇

Function components instead of views

Controllers now render templates via format-based Elixir modules (the announcement still calls them view modules).

Suppose we have the following route:

 get "/greet", GreetController, :hello

That means we need a GreetController with a hello action (I know, it’s not restful, but it’s just an example! 😉)

Our controller code doesn’t change (we could use the params and define assigns, but you get the idea):

defmodule ScoutWeb.GreetController do
  use ScoutWeb, :controller

  def hello(conn, _) do
    render(conn, :hello)
  end
end

Now here’s where things get different.

Before Phoenix 1.7, our controller would expect a GreetView with a render("hello.html", assigns) function, or our GreetView would expect to find a hello.html.eex template under the views/greet/ directory.

Now, our controller expects us to define a GreetHTML (notice the caps in HTML for the format we’re rendering). And it expects us to define that view-like module next to the greet_controller.ex module:

Additionally, we have collocated the view modules next to their controller files. This brings the same benefits of LiveView collocation – highly coupled files live together. Files that must change together now live together, whether writing LiveView or controller features.

The GreetHTML needs to use ScoutWeb, :html and:

  • Define a hello function component inside GreetHTML, or
  • Collocate a hello.html.heex template based on what embed_templates defines.

Let’s take a look at both.

Function component

We can define a hello function component like this:

defmodule GreetHTML do
  use ScoutWeb, :html

  def hello(assigns) do
    ~H"""
    <h1>Hello world!</h1>
    """
  end
end

Notice how we can include the ~H sigil directly inside the hello function. That’s because use ScoutWeb, :html declares our GreetHTML to use Phoenix.Component. If you’ve used LiveView, that should look eerily familiar.

But maybe inline components aren’t your style. No worries.

Embedding templates

Our GreetHTML can specify where to find our templates (instead of relying on the views/greet/ convention). But keep in mind that the idea is to collocate our templates next to our GreetHTML module!

So, we specify where to find the templates with the new embed_templates macro:

defmodule GreetHTML do
  use ScoutWeb, :html

  embed_templates "greet/*"
end

We can now create a greet/hello.html.heex template and include the following:

<h1>Hello world!</h1>

As you can see, everything is now defined under the controllers/ directory:

lib/scout_web/controllers
├── greet_controller.ex
├── greet_html
│   └── hello.html.heex
├── greet_html.ex

I’m not sure I like it all being under controllers since that directory no longer solely contains controllers, but maybe I’ll grow accustomed to it (or, perhaps, we can find a different name in time). But I think it’s great to have everything in the same place.

And I like that it works very similarly to LiveView—where we either render the component inline via ~H or create the template next to the LiveView by default.

Did we lose any View goodness?

The beauty is that I don’t think we’ve lost the nice things from Phoenix.View.

For example, if we reference a function in the collocated template, it should be defined in that GreetHTML module—which acts like our view modules did before.

So, if we were to have the following in our hello.html.heex template:

<h1><%= greeting() %></h1>

We would define that function inside the GreetHTML module:

defmodule GreetHTML do
  use ScoutWeb, :html

  embed_templates "greet/*"

  def greeting do
    "Hello world!"
  end
end

How do I migrate my codebase?

The changes in Phoenix 1.7 are backwards compatible. If you want to keep using Phoenix.View, you can do so by adding :phoenix_view as a dependency.

But if you want to migrate to the new component-based format, you can follow Phoenix.Views’ guides on migrating to Phoenix.Component.

Want my latest thoughts, posts, and projects in your inbox?

    I will never send you spam. Unsubscribe any time.