Trying to follow the pattern based on mix phx.gen.live

First here is a gist with all the relevant code.

If I was to use mix phx.gen.live Blog Post posts title:string body:text I would see that the handle_params is what does the lookup for the record via the context.

In my show example in my gist https://gist.github.com/joshchernoff/8f49a1ff56623d9dca3e021bdc2e8f3b#file-show-ex

I first try and get a current_user if there is one on my mount, then I use that current_user as a way to authorize the user who is looking up the post. Its possible the post is not published yet. (IE: draft) and anyone not an admin will (I hope, given Ecto.NoResultsError is handled) get a 404

My observation though at this point is a redundancy of callbacks.

Reviewing my logs I see not only am I making two calls to mount which is expect but I also make two calls to handle_params.

Here is the whole of my logs when making a single request to a post’s show view.

[info] GET /posts/foobar
[debug] Processing with Phoenix.LiveView.Plug.show/2
  Parameters: %{"slug" => "foobar"}
  Pipelines: [:browser]

[
  "mount",
  %{"slug" => "foobar"},
  %{
    "_csrf_token" => "...",
    "user_token" => ...
  },
  #Phoenix.LiveView.Socket<
    assigns: %{
      flash: %{},
      live_action: :show,
      live_module: MorphicProWeb.PostLive.Show
    },
    changed: %{},
    endpoint: MorphicProWeb.Endpoint,
    id: "phx-Fgvs1dGJIPBNOgEC",
    parent_pid: nil,
    root_pid: nil,
    router: MorphicProWeb.Router,
    view: MorphicProWeb.PostLive.Show,
    ...
  >
]

[debug] QUERY OK source="users_tokens" db=23.2ms idle=3075.7ms
SELECT u1."id", u1."email", u1."hashed_password", u1."confirmed_at", u1."admin", u1."inserted_at", u1."updated_at" FROM "users_tokens" AS u0 INNER JOIN "users" AS u1 ON u1."id" = u0."user_id" WHERE ((u0."token" = $1) AND (u0."context" = $2)) AND (u0."inserted_at" > $3::timestamp + (-60::decimal::numeric * interval '1 day')) [..., "session", ~U[2020-05-04 20:21:36.666142Z]]
[
  "handle_params",
  %{"slug" => "foobar"},
  "https://localhost:4001/posts/foobar",
  #Phoenix.LiveView.Socket<
    assigns: %{
      current_user: #MorphicPro.Accounts.User<
        __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
        admin: true,
        captcha: nil,
        captcha_return: nil,
        confirmed_at: ~N[2020-04-22 20:51:33],
        email: "jchernoff@morphic.pro",
        ...
      >,
      flash: %{},
      live_action: :show,
      live_module: MorphicProWeb.PostLive.Show
    },
    changed: %{current_user: true},
    endpoint: MorphicProWeb.Endpoint,
    id: "phx-Fgvs1dGJIPBNOgEC",
    parent_pid: nil,
    root_pid: nil,
    router: MorphicProWeb.Router,
    view: MorphicProWeb.PostLive.Show,
    ...
  >
]
[debug] QUERY OK source="posts" db=5.9ms idle=2959.3ms
SELECT p0."id", p0."body", p0."draft", p0."excerpt", p0."published_at", p0."published_at_local", p0."slug", p0."title", p0."large_img", p0."thumb_img", p0."likes_count", p0."tags_string", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 WHERE (p0."slug" = $1) ["foobar"]
[debug] QUERY OK source="tags" db=12.0ms idle=2928.6ms
SELECT t0."id", t0."name", p1."id" FROM "tags" AS t0 INNER JOIN "posts" AS p1 ON p1."id" = ANY($1) INNER JOIN "post_tags" AS p2 ON p2."post_id" = p1."id" WHERE (p2."tag_id" = t0."id") ORDER BY p1."id" [[12]]
[info] Sent 200 in 46ms
[info] CONNECTED TO Phoenix.LiveView.Socket in 227µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Parameters: %{"_csrf_token" => "...", "vsn" => "2.0.0"}
[
  "mount",
  %{"slug" => "foobar"},
  %{
    "_csrf_token" => "...",
    "user_token" => ...
  },
  #Phoenix.LiveView.Socket<
    assigns: %{
      flash: %{},
      live_action: :show,
      live_module: MorphicProWeb.PostLive.Show
    },
    changed: %{},
    endpoint: MorphicProWeb.Endpoint,
    id: "phx-Fgvs1dGJIPBNOgEC",
    parent_pid: nil,
    root_pid: #PID<0.683.0>,
    router: MorphicProWeb.Router,
    view: MorphicProWeb.PostLive.Show,
    ...
  >
]
[debug] QUERY OK source="users_tokens" db=7.2ms idle=3347.1ms
SELECT u1."id", u1."email", u1."hashed_password", u1."confirmed_at", u1."admin", u1."inserted_at", u1."updated_at" FROM "users_tokens" AS u0 INNER JOIN "users" AS u1 ON u1."id" = u0."user_id" WHERE ((u0."token" = $1) AND (u0."context" = $2)) AND (u0."inserted_at" > $3::timestamp + (-60::decimal::numeric * interval '1 day')) [..., "session", ~U[2020-05-04 20:21:37.128194Z]]
[
  "handle_params",
  %{"slug" => "foobar"},
  "https://localhost:4001/posts/foobar",
  #Phoenix.LiveView.Socket<
    assigns: %{
      current_user: #MorphicPro.Accounts.User<
        __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
        admin: true,
        captcha: nil,
        captcha_return: nil,
        confirmed_at: ~N[2020-04-22 20:51:33],
        email: "jchernoff@morphic.pro",
        ...
      >,
      flash: %{},
      live_action: :show,
      live_module: MorphicProWeb.PostLive.Show
    },
    changed: %{current_user: true},
    endpoint: MorphicProWeb.Endpoint,
    id: "phx-Fgvs1dGJIPBNOgEC",
    parent_pid: nil,
    root_pid: #PID<0.683.0>,
    router: MorphicProWeb.Router,
    view: MorphicProWeb.PostLive.Show,
    ...
  >
]
[debug] QUERY OK source="posts" db=11.5ms idle=2211.0ms
SELECT p0."id", p0."body", p0."draft", p0."excerpt", p0."published_at", p0."published_at_local", p0."slug", p0."title", p0."large_img", p0."thumb_img", p0."likes_count", p0."tags_string", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 WHERE (p0."slug" = $1) ["foobar"]
[debug] QUERY OK source="tags" db=30.8ms queue=0.1ms idle=2214.7ms
SELECT t0."id", t0."name", p1."id" FROM "tags" AS t0 INNER JOIN "posts" AS p1 ON p1."id" = ANY($1) INNER JOIN "post_tags" AS p2 ON p2."post_id" = p1."id" WHERE (p2."tag_id" = t0."id") ORDER BY p1."id" [[12]]

This shows me that I’m not only looking up the user twice but also the blog post in question.
Is there a better way to do this? I feel like I’m missing something here. I understand that some of the callbacks get called twice as a result of the http request and the follow up websocket calls, so if thats the case whats the best approach for this? If this is it, then I guess I’m just looking for validation.

4 Likes

First off, this is an absolutely A+ question forum post. This should be pinned somewhere as an example. You’ve got code, you’ve got logs, you’ve got everything, it’s great.

Second:

You’re doing everything right, this is just what happens on the first websocket connect. The whole page is built from scratch twice, with all that that means logs included. However if you start using live_redirect to link between pages, then you only pay this penalty on the first page the user lands on. From there, subsequent page loads are done exclusively via the active live socket, and so only need to happen the usual one time.

8 Likes

Ah thank you for explaining the context of live_redirect I’m in the process of migrating my blog over to LiveView so I have been piecemealing LiveView and I had not yet considered the case where I’m not re establishing the web socket. That makes much more sense now as to the trade offs. Thank you !

1 Like

This really tells me that even though I can embed LiveView in a normal controller workflow I’m much better off trying to build my app holistically in LiveView to get the full benefit.

This maybe a good word of warning to those who are migrating to LiveView since most migrations are done small bits at a time. I would suspect this would address why I feel like I’ve seen this question come up a lot, though to be honest this is the first time I think I’ve seen any emphasis on using live_redirect.

1 Like

Well, as always, it depends. Consider a situation where you have an ordinary form, but you want to jazz up some of the form interactions with live view. The controller will load all kinds of values from the DB that the liveview doesn’t really need in order to do its work. The form submission will be handled by a different controller POST action that looks up all those values all over again anyway, so there’s no point in validating the user in the live view. There are a lot of narrowly scoped live view uses that don’t have to worry about this at all.

Good, point. I guess identifying when I crossed that boundary was where I started to struggle. Thanks for all the very helpful info.

1 Like

Right, if you move from having LiveView manage some small interactions on a page to having it be the page, you do essentially cross a boundary. Once you start having pages that are over that boundary there are some nice benefits to staying within that boundary. If you’re regularly crossing over from live to non live pages, you’ll incur some additional load re-fetching stuff.

1 Like