How to access the current_user inside of my live_component?

I generated two sets of scaffolds: one for phx.gen.auth, and one for phx.gen.live for a model called Article. I am using Phoenix 1.7.0. I am trying to access the current_user inside of my live_component (which was generated by phx.gen.live). In particular, I have a reference field on my Article model that I am trying to assign to the current_user in new and edit actions in my form_component.ex file. How can I accomplish this?

1 Like

When you create the user you can add that to socket.assigns. A live component is on the same process as the live view so you should be able to access the state.

Where exactly do I add it to the assigns? I tried assigning it in the mount function of index.ex, but I still can’t seem to access it in the form_component.ex actions. Do I need to assign it in every endpoint along the way as well?

1 Like

Live components have a callback mount(socket)

OK, figured it out. Thanks for your help!

Happy to help!

What did you do?

From the docs:

A LiveComponent is rendered as:

<.live_component module={HeroComponent} id="hero" content={@content} />

You must always pass the module and id attributes. The id will be available as an assign and it must be used to uniquely identify the component. All other attributes will be available as assigns inside the LiveComponent.

So if you need to access anything in your live component just pass it as an attribute to the live_component helper. If the live component is shared by Index and Show live views for example, both of them need to pass the assigns required by your live component.

To enforce this you can add in the live component something like:

def update(%{user: user, sponsor: _} = assigns, socket) do
....
end

This will raise an execption if those assigns are not passed by the parent view.

You can access them in your live component within assigns or socket.assigns.

I can pass @current_user from the LV to the <.live_component current_user={@current_user}>. Is there a way to access @current_user without passing it in as an attribute?

Unfortunately, I’m not aware of any other way of doing this that’s as simple and straightforward.

While reading through the live component docs, I find there are many ways of communication between a live component and its parent LV, but I’m not sure if there is a simpler solution for your use case.

The answer is sort of yes but really no. If you want something like React’s context, you can check out Surface. I actually don’t know the implementation details but I assume it’s passing the state around via processes, so you could implement that yourself in theory. You could also just make a database call and grab the user again, which is more clearly a bad idea. Passing stuff around is the very nature of functional programming. You can avoid excessive “prop drilling” by using composition, that is, make liberal use of @inner_block and slots. This leads to far more navigable templates.

Of course this is just in my experience. In the only React project I worked on that used contexts, we just ended up abusing them. Our template hierarchy ended up looking like an OO-esque rabbit hole. I know it’s possible to use contexts responsibly, but I haven’t personally missed them. I have no idea if they are on the HEEx roadmap or not.

This!

I think any time you have excessive prop drilling is a sign you built fragile and poorly designed UI components that encapsulate too many concerns and assumptions when you should have used slots to invert the control. This serves to keep the depth of prop drilling to a minimum.

A classic is navigation / UI shell where you could bake in the navigation menu system, the alerts and notifications drawer, the user profile, the breadcrumbs, the user avatar, the login state/logout and profile settings menu into a shell monolith component. The alternative is to provide semantic components to describe the shell where the LiveView passes just the props needed to each component.

Composition vs encapsulation.

2 Likes

That leads to breaking the functional paradigm as functions are inte ded to take all the data they need explicity as arguments and return a result.

What you are asking for is some kind of “global” ambiant state. There is the process dictionary where things can be stashed however it’s usually a bad idea to use it and can lead to surrprises and testing difficulties.

Don’t do it. Just learn to put the concerns in the right place and be explicit about passing arguments, because soon enough you will build entire contexts and Ecto queries based on this ambient state because you want to enforce query scopes and permissions for a user, so why not “just use the user in the process dictionary”? Now testing gets harder and requires setting up the ambient state, and then any use of agents or passing work to another process even via say Oban becomes a problem because that ambient state won’t exist in the wroker process.

It’s a path that leads to ruin, resist the temptation.

As Oscar Wilde said “I can resist anything except temptation”.

2 Likes

So what I am doing is that I wrote a helper function default_assigns/1 that I use in all of my LiveViews that render components and also within the components mount()/update() callbacks this way:

defmodule UI.Live.Helpers do
  def default_assigns(assigns) do
    {current_user: assigns.current_user, current_company: assigns.current_company, current_store: ... }
  end
end

Then I call it in LiveViews/components that render other Components:

<.live_component module={UI.MyComponent} id="my-componet" {default_assigns(assigns)} />
or
<.my_component {default_assigns(assigns)} />

And in the mount or update I do something like:

socket
|> assign(default_assigns(assigns))

I am unfortunately doing it everywhere which is annoying but works.

You might hurt LiveView change tracking with your approach passing a map as any change will need to send all the data rather than just the changed values.

This article might be useful:

Additionally because live view can’t detect if something changed because you’re using a derived value, it will mean LiveView has to always render those components using the helper. Every render will always result in the derived_assigns function being called to compute the map, even if nothing has changed, where if you passed values directly using tenant=@tenant user=@user then there is no need to compute a derived value (i.e the map), LiveView will detect nothing has changed and not even call render, while it cannot do that with you’re default_assigns function.

1 Like

The new way of handling the “default_assigns” thing this is to use %Scope{}, which is kind of like Rails’s Context (if you’re familiar) except you pass it explicitly as opposed to it being a global thing. IE, it’s a bag of data your app needs everywhere that you can pass around, or pass around parts of in regard’s to @adw632’s points. I know it’s not in the generators yet, but it’s literally just defmodule Scope, do: (defstruct [:tenant, :user]).

1 Like

I’m not passing a map anywhere and these assigns do not change since the initial values are fetched down in the Plug. I think I misunderstood the meaning of my “helper function” and thought that I am using it to get the output into HTML which I am not.

is this documented anywhere? Or is it just a struct you initiate in Plug and then you have to pass around from parent LiveView to Components it renders and then their child Components?

Chris talked about it at the most recent keynote and there is an example in todo_trek: todo_trek/lib/todo_trek/scope.ex at main · chrismccord/todo_trek · GitHub. It’s really nothing more than a plain struct for carrying around globally pertinent entities, ie, your app’s “scope”. I used to make myself such an object in Rails before it introduced Current called ApplicationContext, which was a little long, so Scope is pretty nice!

2 Likes

Oh I see. And then you use it this way:

That makes sense. And maybe indeed like @sodapopcan is actually right that the default_assigns(assings) will have negative performance implications, but the %Scope{} approach is even cleaner so I’ll switch to that!

1 Like