How do I pack a list inside a string so that I can send it as a param in live_redirect?

The problem is going from login (normal controller) and live view… I see.

But still there are many ways to store those intermediate data.

An example… give a session_id, with the plug I already show You.

Use this key to store data when You leave live view, to go to the login controller… into an external store.

The simplest case would be an Agent, to store data per session_id key.

When You return to live view, retrieve data from the Agent, using the session key.

1 Like

OK … so then my two choices for this really are:

  1. Find a way to pack the session data (all my filters that are in the List described above in the first post) into a string and then pass that as a parameter in the redirect function.
    OR
  2. Temporarily store the session data in a log file or database. Use a user_id if the user has logged in or create a uniuqe ID myself and call it session_id.

Is that correct?

You might not know them yet, but OTP has tools to store data. ETS, GenServer, Agent.

Take a look at what an Agent is here… Agent — Elixir v1.12.0

It is a simple solution.

One more question … can _csrf_token be used as a unique identifier for the session?

@kokolegorille already mentioned the conventional method. My method is unconventional but if you are not happy with the conventional method you can try this:

  • before the first liveview redirect the user to login form, store everything you want to carry over to the client side, by push_event them over.
  • login, etc
  • after the user landed in the second liveview post-auth, grab everything back from the client side.

You do not need cookie, session, query string, etc to make it work. heck, you don’t even need a database for this.

1 Like

Here is a super simple agent…

defmodule KvStore do
  use Agent
  def start_link(initial_value \\ %{}) do
    Agent.start_link(fn -> initial_value end, name: __MODULE__)
  end

  def store(key, val) do
    Agent.update(__MODULE__, &Map.put(&1, key, val))
  end

  def get(key) do
    Agent.get(__MODULE__, &Map.get(&1, key))
  end
end

and how to use it…

iex(1)> KvStore.start_link
{:ok, #PID<0.185.0>}
iex(2)> KvStore.store("1234", %{filter: "koko", page: 1, search: "koko"})
:ok
iex(3)> KvStore.get "1234"
%{filter: "koko", page: 1, search: "koko"}
2 Likes

You can also pass those params to the login controller, and have them back in the mount(params, _, _)

Sure, it is the conventional way. I hate ugly long URL though.

Anyway I don’t think it’s related to Phoenix, or live view. You would have the same problem with Rails, and it would probably be harder to solve, because You don’t have those facilities that OTP provides :slight_smile:

2 Likes

So there’s a much easier way to send data between liveviews and it’s using the pubsub built into Phoenix and the broadcast function. I’m out and about on my phone at the moment, but here’s an article about it.

There’s a lot of info there, but it’s actually pretty easy to add. There’s three steps:

  1. Subscribe to a pubsub topic in the liveview on mount. Make your topic unique to the session/user (maybe csrf token?) unless you want multiple people to receive the same updates.

  2. Broadcast to that topic when you want to talk to other liveviews. In your example, when they’ve logged in, you might want to broadcast the user details to the other liveview, or just let it know and it can broadcast the cart items back.

  3. Have a function that handles broadcasts (handle_info instead of handle_event) and do whatever you need to when they come in.

So let’s say you have a cart liveview and a login liveview.

  1. You add items to the cart, stored in LV1.

  2. You go to purchase, which requires login (LV2). You broadcast to LV2 to tell it to appear.

  3. They login, and LV2 broadcasts the user session details to LV1 (and any other LVs listening to that pubsub).

  4. Now LV1 has everything it needs to handle a logged in purchase.

There’s a ton of different ways you could handle this flow, but yeah broadcasting can be a great way to communicate between liveviews. You could also componentize all of this and have the necessary data flow up to a parent liveview and down to other components that way.

1 Like

I agree. I don’t like long URLs but I think it will be the fastest and easiest way for me to solve this. But I’ll just pack ID numbers into the url to make it as short as possible.

I was hoping I could just pack the list detailed above into a string and then just unpack it on the other end. That would have made things faster for me. But it sounds like there is not easy way to do that so I’ll just pack and unpack the ID numbers into a string. Not pretty, but it will work.

Could you use :erlang.term_to_binary and :erlang.binary_to_term? I haven’t fully understood the use case but at face value that might work

Thank you. This helped so much!

Thank you for this info. For some reason, I missed this when you first posted it so I just saw it now. I’m going to definitely do more research on PubSub and OTP.

@carterbryden Thank you for your previous post. I am finally at a point where I feel strong enough in Phoenix to tackle PubSub. When you posted this before, I was on such a steep learning curve that I couldn’t tackle PubSub. Now I think I’m ready. I’ve read your post over as well as a bunch of documentation.

I have three questions about using a unique identifier as my topic so that I keep the PubSub limited to one user:

  1. Can I use the session_id from session as my Topic identifier? It appears to be the same whether the user is logged in or logged out and is also the same when moving to a new LiveView. I can see the csrf token (that you mentioned), but that seems to change after the user logs in.

  2. With Phoenix 1.6, should I be using mount or on_mount to subscribe to a topic?

  3. When LV2 mounts, can it ping the PubServer for any previous messages that it missed? I didn’t fully understand your comment above when you said, “You broadcast to LV2 to tell it to appear.” LV2 won’t exist until LV1 invokes it with a live_redirect. So I need LV2 to fireup, subscribe to the topic and then grab the data that was broadcast to the PubSub server by LV1.

I think that I misunderstood your goals in my original reply, and if you’re using live redirects then broadcasting with pub sub is probably not what you actually need here.

Instead, what you probably want is to store the state (say, items added to a cart) either server side in some way or client side in local storage (JavaScript). For server side, you could use ets, a database, or something like that.

For the shopping cart use case, I’d probably pick ets for simplicity and store the state with a timestamp so that I could delete old carts after a bit (otherwise memory usage will grow). You could make the key for that ets entry the session ID as you mentioned, I think. Then after logging in, you can grab the state out of ets again on mount or whenever works best. Elixir school has a good article about using ets.

Or you could save the state to the client local storage and pull it out later after login. You could send it to the server with pushEvent.

Alternatively, you could create a componentized login form that logs you in with an ajax request or a liveview or something, without leaving the page. Not necessarily a live_component (though it could be), just a partial of some kind that you can put on any page. In that case, you could have multiple liveviews on the same page and send messages between them with pubsub (because they’d both exist at the same time).

There’s nothing that says authentication has to require a page change, but it can be a painful modification to make sometimes if you’re already doing it that way, especially if you’re using a library. So I’d probably use the first method I mentioned and store/retrieve the state in ets (server side) or local storage (client side).

1 Like

I explored client side storage but that is difficult in LiveView since I only have access to the session on mount. It’s looking like I need to either use ETS or GenServer. @kokolegorille helped me with an Agent above and that’s what I’ve been using. I’m just not sure how Agent will scale. I don’t have a good sense for when to use Agent vs ETS vs GenServer. I am hesitant to use ETS because of the warning in the Phoenix HexDocs:

"We don’t recommend using this store in production as every session will be stored in ETS and never cleaned until you create a task responsible for cleaning up old entries.

Also, since the store is in-memory, it means sessions are not shared between servers. If you deploy to more than one machine, using this store is again not recommended."

Kind of makes me nervous, but I could add a cleanup mechanism using the method you recommend above and worry about an alternative if I see any performance issues. If I have that many users, then it’s a great problem to have! :slight_smile:

Thanks for the detailed advice. Really appreciate it!

Adding these resources for anyone else who stumbles on this post and is struggling with the same issue:

Caching Options in an Elixir Application

Real world usecase for GenServer and Agent in Elixir

Elixir Agent vs GenServer

When are Agent and Task.Supervisor Useful?

This was posted a while ago, about how to store state client side and restore it on mount time.

1 Like

Thank you! I’ll give that a read!