LiveViewでTaskの結果はhandle_infoで受ければよいという話
ElixirWeekly の何号だったか失念してしまったのですが。
LiveView のプロセスで、非同期処理を Task.async/1
で実行したならば、Task.async/1
が終了時に送信するメッセージを受け取ればよい、というお話。
プロセスのふるまいを学んだときに、どのようなメッセージが送られるか問いことを覚えたはずなのに、文脈が変わっただけでこうも気付けなくなるものかと思い知った次第。
おさらい Task.async/1
Task.async/1
を実行すると、Task
構造体が返ります。
構造体の中のキー :ref
にタスクを識別するリファレンスが格納されています。
iex(1)> Task.async(fn -> :timer.sleep(100); 123 end) %Task{ owner: #PID<0.109.0>, pid: #PID<0.111.0>, ref: #Reference<0.1023354925.2953576457.207757> }
タスクが終了すると、リファレンスとタスクの最後の値のペアがメッセージとして送られてきます。
最後にタスクのプロセスが終了したことの通知 :DOWN
が送られます。
iex(2)> flush {#Reference<0.1023354925.2953576457.207757>, 123} {:DOWN, #Reference<0.1023354925.2953576457.207757>, :process, #PID<0.111.0>, :normal} :ok
これをふまえて
初期状態 stopping
のときに START
ボタンを押すと、starting
と表示されのち1秒後に running
の表示に換わるサンプルです。
Task.async/2
の戻り値に含まれるリファレンスを保存しておきます。
受信したメッセージにタスクのリファンレンスが含まれていれば、先に起動したタスクの結果のメッセージなので、処理をします。
プロセス終了時の :DOWN
を含むメッセージも送られてくるので、そのメッセージにマッチする handle_info/2
を用意しておかなければなりません。
ここでは他のすべてのメッセージは無視するようにしています。
defmodule MyAppWeb.PageLive do use MyAppWeb, :live_view def mount(_params, _session, socket) do {:ok, assign(socket, status: "stopped", ref: nil)} end def render(assigns) do ~H""" <button phx-click="start">start</button> <div><%= @status %></div> """ end # ボタンの phx-click="start" のイベントハンドラ def handle_event("start", _, socket) do task = Task.async(fn -> # (時間のかかる処理の代わり) Process.sleep(100) 123 end) {:noreply, assign(socket, status: "starting", ref: task.ref)} end # タスクの結果を受け取る # (socket に格納したリファレンスと受信したメッセージに含まれるリファレンスが一致をチェックする) def handle_info({ref, _result}, socket) when socket.assigns.ref == ref do {:noreply, assign(socket, status: "running")} end # :DOWN のメッセージ等を無視する def handle_info(_, socket) do {:noreply, socket} end end