Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: component unmount callback #1238

Closed
byronanderson opened this issue Nov 22, 2020 · 5 comments
Closed

Feature request: component unmount callback #1238

byronanderson opened this issue Nov 22, 2020 · 5 comments

Comments

@byronanderson
Copy link

I saw in another issue how to find out when the live view itself is unmounted, but I don't see that the same mechanism can be used to know if a component is unmounted.

I'm trying to build the react hooks interface on top of live component and I don't know how to have an effect clean up after itself if I don't know when the component itself is being cleaned up.

@josevalim
Copy link
Member

Can you please explain the use case in particular? Sure, React has them, but why would they be necessary in LV which server side? Which kind of functionality could you only implement with it? Thank you.

@byronanderson
Copy link
Author

byronanderson commented Nov 22, 2020

Yep for sure:

The toy example I was playing with was cleaning up after a :timer.apply_interval call, to put the current time into the state of the component, for example.

More realistically, I'm trying to let components have more ability to subscribe to things directly:

  1. A conditionally rendered component takes an id as a input, and subscribes to data for that id.
  2. The parent component or view then chooses to stop rendering the component.
  3. I would like to then have the component able to know to unmount that subscription.

You are right to push back on it being as fundamental as it is in the browser environment - I went to usehooks.com to see how many of those effect hooks made sense in the context of live view, and largely they do not, since they subscribe to native browser APIs that aren't available from a server environment.

The interface that I am currently targeting for a "component that is implemented with react-style hooks" looks like this:

defmodule DemoWeb.Counter do
  import DemoWeb.HookComponent.Hooks # gives me use_state and use_effect
  import Phoenix.LiveView.Helpers

  def describe(assigns, hooks) do
    {count, set_count, hooks} = use_state(0, hooks)

    hooks =
      use_effect(
        fn ->
          {:ok, ref} =
            :timer.apply_interval(1000, DemoWeb.Counter, :_block_timer_apply, [
              fn ->
                set_count.(fn count -> count + 1 end)
              end
            ])

          # the effect returns a function that says how to "cancel/undo" the effect
          fn ->
            :timer.cancel(ref)
          end
        end,
        [],
        hooks
      )

    {
      ~L"""
        <div style="color: red"><%= count %></div>
      """,
      hooks
    }
  end

  def _block_timer_apply(func) do
    func.()
  end
end

I have it largely working with a small patch to the outermost liveview, but an abandoned Counter component would cause more timer functions to apply unnecessarily.

A worse example in this case might be:

The counter is mounted, unmounted, and re-mounted, so there are two intervals applying updates to the counter, doubling the rate of the counting. I haven't tried it though, and maybe "remounting" a component with the same id isn't a thing in live view?

@josevalim
Copy link
Member

I see. You have a good point about unsubscribing but the issue is that, since the component does not receive messages, it always needs coordination from the parent. Things like apply_interval is also problematic, because IIRC the function runs in a separate process and if it blocks, it becomes a bottleneck.

I wonder if your particular could be solved with send_update_after. This means the component is always the one receiving and scheduling new ticks, it is more performant, it doesn't require the coordination with the view, and if it the component "terminates", it simply stops scheduling new ticks.

@chrismccord
Copy link
Member

While this is technically possible for us to implement for components, I'm not convinced yet on a valid usecase. As José said, process-based side effects don't fit to be handled here. Thanks!

@AHBruns
Copy link
Contributor

AHBruns commented Feb 11, 2023

Update: I made a new issue to better explain my use case + not revive a dead issue.

@josevalim, in my recent issue (#2445) I noted that React had moved away from context because it now provides a way for arbitrary external stores to be accessed in components, and more importantly, for those component's to subscribe to state changes in those external stores. This API is called useSyncExternalStore.

While I would certainly like to explore adding such a feature to live view (it's more generic and powerful than a bespoke context API), I think implementing an async version is almost possible today with send_update. The only issue is that send_update has no way to talk to all instances of a given live component. So, live component instances need to register themselves. This actually works fine, but because live components have no unmount callback it is impossible for them to unregister themselves.

If this callback were to be implemented, we could have a user space implementation of send_update which broadcasts an update to all instances of a live component that are currently mounted. This would allow a really nice pattern for libraries where they could provide an on_mount lifecycle hook instrument the root live view, and then provide live component(s) which send and receive updates from the root live view. Also, it's not context because it's async and push based. All the state being passed around would be explicit.

As of now the only way this could be achieved in user space is, some what ironically, by abusing the JS hooks destroyed callback. Obviously that's a terrible idea, but I might implement it to show how the pattern (not the implementation) can be used.

Also, @chrismccord, I'd be interested if you see this as a valid use-case. I think it's better than the original example in that it doesn't have anything to do with trying to treat live components as pseudo-processes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants