Passing data between components: how to truly update socket.assigns state?

Within a single parent live view I want to move from one component back to another via a shared parent. I need a newly set socket state to prevail. I’m using push_patch. I thought the reload: true option was what I needed, but adding it has no effect. :frowning:

Why not put it in the query params? Since it’s a list and/or a changeset. Trying to do it causes errors.

Seems like a common enough process that I must be missing something. Also misunderstanding what the “patch” being pushed is. How can I make this work the right way?

So I add my new data to the socket. This is the workflow:

  • CHILD: add new data to the socket in child component
  • CHILD: redirects back to the parent component Index.
  • PARENT: Call received. mount is skipped and only handle_params is called
  • PARENT: socket is examined and new data is not present
Code
# inside CHILD component I want to switch, but it passes through the parent first, without my new data

  socket = # assign the data
      socket
     |> assign(:existing_users, existing_users)
     |> assign(:user_changeset, user_changeset)
{:noreply,
     socket # go back to parent now
     |> push_patch(to: Routes.user_index_path(socket, :display)}

# PARENT component - data is not persisting that I just added
  def handle_params(params, _url, socket) do
   IO.inspect(socket.assigns.user_changeset) -> nil
   IO.inspect(socket.assigns.existing_users) -> nil

    #  Data from original `mount` is still available only
    end
#

Note: The app is only 6 months old but it looks it’s using liveView version 17.5

Edit: If I call handle_info on the parent component manually inside child, MyAppLive.IndexComponent.handle_info(:test, socket) I can see the socket data is persisting. I’m surprised this doesn’t cause a circular error though. Is this a valid operation type?

Hmm, could you give a more concrete example of what you’re trying to achieve?

I’d suggest reading through the Managing State section of docs for Phoenix.LiveComponent which covers how to message the parent LiveView via send(self(), {:updated_card, message}) or a child LiveComponent via send_update(MyLiveComponent, data).

1 Like

It sounds like you’re assigning data on the child component and expecting that date to be available on the parent. Thing is, they’re two different sockets. As per @codeanpeace’s advice, use send to send the data from the child to the parent and assign it there.

You don’t call another module’s handle_info, you send it a message and write a handle_info to handle that message in the receiving module.

3 Likes

Thanks for the ideas!

  • Trying to save the state of the child component on the parent since it may be needed again
  • in the same action, redirect away to do stuff
  • then maybe come back to child and access the prev state.

I’m able to get the send to work on it’s own but as soon at I add the “redirect” push_patch after it, things get out of sync.

Is there an async send that I can nest the redirect in to make it wait? I only found an async send_update which is the opposite of what I need.

# child
send(self(), {:display, %{existing_users: existing_users}) # calls handle_info 
socket
 |> push_patch(to: Routes.user_index_path(socket, :display)} # calls handle_params

# parent - this called after push_patch, so the data added here is not available in the next call
def handle_info({:display, existing_users}, socket) do
    socket =
      socket
      |> assign(:existing_users, existing_users)
    {:noreply, socket}
  end
 # parent - called by push_patch, runs before the above is comlpeted
 def handle_params(params, _url, socket) do
   # need to access the socket.assign data from the handle_info but it does not exist
end

I need the handle_params to run only after the socket has been updated with the new data.

send is not working super reliably for me in general. I’m needing to restart the server since it stops receiving sometimes. TBF I have this same issue with pubsub.

I really didn’t want to redesign my whole solution but looks like I might have to.

There are really two main ways.

  • the assigns are passed in via the mount/update callbacks
    Think trickle down props (smart trunk, stupid branches)

  • the assigns as passed in via send_update
    Using a pid you can push a new update call and pass it explicitly the assigns.
    This is commonly used on components that manage their own state rather than taking the state passed in via the props which are normally trickled down from the parent component.

There maybe other ways but this is your two most common use cases.

Edit: handle_params will fire the update callback and is also typically handled at the top most component and or use send_update to push that state to a desired component. Given the update callback is on the top most parent this normally cause a cascade of update callbacks for the dependent children components.

Hi @polypush135,

But I’m going this other way in this one case, from child to parent. Trying to push some data “up”, “save” it on the parent, then move on.

I think I’ll need a redesign where this isn’t the case.

Careful to not cause a loop given you are pushing to the parent.
If you ever done react + redux you will know this level of bi directional state causes huge issues.

Again typically its top down, but you can push to any given component via the send update, just beware you may cause a loop if you push to the parent that also supplies the same component in question.

Edit: One last thought, its possible to cause the handle_params to be the trigger vs send_update and push navigation as a way of triggering the update callback. Its common to let handle_params be the first of the events to cascade.

Edit Edit:
Look at how the form module does this via the generated scaffold code, its an example of the form module pushing to the parent to let it know its been submitted. Though it does not use handle params.

Maybe don’t patch from the component? You could try doing it from the handle_info. But I think you need to step back and think about your design.

It does seem odd that you’ve got the list of users in both the parent and child and your updating the parent by sending the entire list that has changed only in the child to the parent. Are you not then assigning that to the child and thus in some kind of weird assign loop?

You might need to share a lot more of the code so people can see what’s really going on.

2 Likes

So I’ve got it working. I think I’ve got it far enough that I won’t have to go into the weeds and post large amounts of code.

The gist is that the child component (#1) produces a list but doesn’t use the data itself. It only takes user input and processes it, which produces the list.
Then it needs to pass this list to another child component (#2), via the parent. But what if the other child (#2) needs to go back? This has been the whole problem, that state is gone and could not be saved.

I guess the solution would be to move all the computations up to the parent, and thus allow things to be passed down instead. Like, the child takes the user input (as now), but passes input to the parent instead of doing any processing here. Then parent produces the list of data and has the state in situ.

I’m surprised I haven’t run into this problem yet in 6 mos of using Phoenix. It’s b/c all other children save things to the DB, which propagates easier, and nothing goes “up.”

For now I have a kind of hack job that makes this work

#  Child 
# - when a list of users is made, send it up to the parent 
# - include the route that we are redirecing to as a param redirect_to
existing_users = result_of_search_func_on_child
user_changeset = result_of_same_kind_of_thing
send(self(), {:users_found, 
%{existing_users_found: existing_users, 
     user_changeset: user_changeset,
     redirect_to: Routes.user_index_path(socket, :display})

# Parent LV 
# - existing_users_found & user_changeset are set to nil state before below runs
def handle_info({:users_found,
  %{existing_users_found: existing_users_found, 
       user_changeset: user_changeset,
       redirect_to: redirect_to
}}, socket) do
     # update the parent state
      socket =
        socket
        |> assign(:existing_users_found, existing_users_found)
        |> assign(:user_changeset, user_changeset)
     # now do the redirect from here to the other child component #2
    {:noreply, #this redirect caused alot of the headaches
      socket
      |> push_patch(to: redirect_to)}
   end

It’s doesn’t seem pretty, but it works for now.

Thanks everyone for all the tips and the help!

Creating a list just to send it on and never use it does seem inefficient.

I don’t think you’ve really explained what “going back” really means in your context. But it sounds like some search thing, which you can do by updating url query params, where back and forward will work.

1 Like