"form_for" vs. ".form" vs "f = form_for"

In various code samples, I see different ways to create a form in a .heex template.

What is the difference between using the following:

<%= form_for @changeset, "/to_path", ...other_opts, fn f -> %>
<% end %>
<%= f = form_for @changeset, "to_path, ...other_opts %>
<% end %>
<.form let={f} for={@changeset} action={"to_path"} ...other_opts>

Is there a best practice, or is it just up to the preferences of the developer?

1 Like

I found some answers in the Phoenix.HTML docs:

  1. <%= f = form_for @changeset ... %> is now deprecated. Use <%= form_for @changeset ... %> instead.

  2. <.form> is a “function component”, in the context of Phoenix LiveViews.

This leads me to a follow-up question: If I don’t use LiveViews, should I still use <.form>?

1 Like

You’re right about the difference. According to the LiveView docs, <.form> is just a wrapper around form_for and should generally be preferred if you’re using HEEx templates (including on deadviews). Just remember to give the action attribute!

See Phoenix.LiveView.Helpers documentation under “Outside LiveView”


Doh! Link is dead

Here you go!
Example: Form Outside LiveView (regular HTTP requests)

1 Like

And for the ones of us running Phoenix.LiveView pre-0.18 (e.g., 0.17.14) there is also the “Outside LiveView” example under the Phoenix.LiveView.Helpers.form/1 documentation.

I know this thread is old, but I was struggling with the same question, so I’ll post the answer in case anyone else comes across this thread like I did.


  • form_for should no longer be used and will eventually be removed from Phoenix.
  • In LiveView, use <.form for={@form}> where @form is a Phoenix.HTML.Form struct (generally created using to_form). Don’t use :let.
  • Outside of LiveView, you can also pass a changeset or map to for, and it’ll be converted to a Form which can be captured with :let. (This approach is also technically possible in a LiveView, but it’s discouraged because it makes change tracking less efficient.)

The second and third points are made clear in the docs for form/1. The first point was confirmed to me by none other than Chris McCord; I wrote about it here:


Correct. .form is the way to go, form_for will forever exist for backwards compatibility, f = form_for is deprecated and should be rewritten.