Я работал с Phoenix LiveView и Surface-UI около года; я хотел бы поделиться некоторыми вещами, которые я узнал трудным путем.
Введение
Марлус Сараива познакомил меня с концепцией сообщества React под названием single source of truth
.
Для любых данных, которые изменяются в приложении React, должен существовать единый
источник истины
. Обычно состояние сначала добавляется в компонент, которому оно необходимо для рендеринга. Затем, если оно необходимо другим компонентам, вы можете поднять его к их ближайшему общему предку.
полная ссылка — Поднятие состояния вверх
Как только вы поймете эту концепцию, вы будете видеть ее снова и снова в качестве паттерна.
Установка
Представьте себе приложение под названием Dummy, в котором есть Калькулятор для температуры, где вы вводите значения в градусах Цельсия и показываете их в градусах Фаренгейта.
Приложение будет выглядеть следующим образом:
Первым делом, мы должны добавить новый путь в Router для нашего живого Калькулятора.
router.ex
scope "/", Dummy do
pipe_through(:browser)
live("/", Calculator)
end
Теперь маршрут создан, и мы можем работать над live_view. Мы можем игнорировать params
и sessions
в mount/3, и просто присвоить значение по умолчанию Fahrenheit через сокет.
В шаблоне render/1 мы вызовем TemperatureInput live_component
и передадим id и значение Фаренгейта. Наконец, мы добавим handle_info/2.
live/calculator.ex
defmodule DummyWeb.Calculator do
use DummyWeb, :live_view
alias DummyWeb.TemperatureInput
@impl true
def mount(_params, _session, socket), do: {:ok, assign(socket, :fahrenheit, 0)}
@impl true
def render(assigns) do
~H"""
<main class="hero">
<.live_component module={TemperatureInput} id={"celsius_to_fahrenheit"} fahrenheit={@fahrenheit}/>
</main>
"""
end
@impl true
def handle_info({:convert_temp, celsius}, socket) do
fahrenheit = to_fahrenheit(celsius)
{:noreply, assign(socket, :fahrenheit, fahrenheit)}
end
defp to_fahrenheit(celsius) do
String.to_integer(celsius) * 9 / 5 + 32
end
defp to_celsius(fahrenheit) do
(String.to_integer(fahrenheit) - 32) * 5 / 9
end
end
Теперь перейдем к компоненту live_component.
live/temperature_input.ex
defmodule DummyWeb.TemperatureInput do
use DummyWeb, :live_component
def render(assigns) do
~H"""
<div>
<div class="row">
<.form let={f} for={:temp} phx-submit="to_fahrenheit" phx-target={@myself} >
<div>
<%= label f, "Enter temperature in Celsius" %>
<%= text_input f, :celsius %>
</div>
<div>
<%= submit "Submit" %>
</div>
</.form>
</div>
<p>temperature in Fahrenheit: <%= @fahrenheit %></p>
</div>
"""
end
def handle_event("to_fahrenheit", %{"temp" => %{"celsius" => celsius}}, socket) do
send(self(), {:convert_temp, celsius})
fahrenheit = to_fahrenheit(celsius)
{:noreply, assign(socket, :fahrenheit, fahrenheit)}
end
defp to_fahrenheit(celsius) do
String.to_integer(celsius) * 9 / 5 + 32
end
defp to_celsius(fahrenheit) do
(String.to_integer(fahrenheit) - 32) * 5 / 9
end
end
Если посмотреть на live_component, то в нем есть только форма с полем ввода, где пользователь вводит значение в градусах Цельсия и нажимает кнопку submit, чтобы отправить это событие на сервер, который имеет handle_event/3, и делает две вещи:
- посылает сообщение родителю (live_view).
- вычислить температуру по Фаренгейту и присвоить ее через сокет.
Мы используем две привязки LiveView phx-*:
Вы, скорее всего, заметили, что handle_info в live_view и handle_event в live_component имеют одинаковый код. Единственное различие между ними — это функция send()
для обновления значения Фаренгейта.
Проблема
Мы должны избегать дублирования логики, а в нашем примере формула для преобразования Цельсия в Фаренгейт дублируется и в live_view, и в компоненте.
Чтобы продемонстрировать, что может пойти не так, мы используем :timer.sleep
для изменения формулы только в компоненте live_component.
Измените логику в live_component.
live/temperature_input.ex
def handle_event("to_fahrenheit", %{"temp" => %{"celsius" => celsius}}, socket) do
send(self(), {:convert_temp, celsius})
# wrong formula
fahrenheit = to_celsius(celsius)
{:noreply, assign(socket, :fahrenheit, fahrenheit)}
end
live/calculator.ex
def handle_info({:convert_temp, celsius}, socket) do
:timer.sleep(4000)
fahrenheit = to_fahrenheit(celsius)
{:noreply, assign(socket, :fahrenheit, fahrenheit)}
end
Измените логику в live_view.
live/temperature_input.ex
def handle_event("to_fahrenheit", %{"temp" => %{"celsius" => celsius}}, socket) do
send(self(), {:convert_temp, celsius})
fahrenheit = to_fahrenheit(celsius)
{:noreply, assign(socket, :fahrenheit, fahrenheit)}
end
live/calculator.ex
def handle_info({:convert_temp, celsius}, socket) do
:timer.sleep(4000)
# wrong formula
fahrenheit = to_celsius(celsius)
{:noreply, assign(socket, :fahrenheit, fahrenheit)}
end
Решение
Избегайте хранения логики в нескольких местах. В данном случае мы решили сохранить логику только в live_view, но она не обязательно должна быть в live_view. Цель состоит в том, чтобы никогда не дублировать логику.
live/temperature_input.ex
def handle_event("to_fahrenheit", %{"temp" => %{"celsius" => celsius}}, socket) do
send(self(), {:convert_temp, celsius})
{:noreply, socket}
end
live/calculator.ex
def handle_info({:convert_temp, celsius}, socket) do
fahrenheit = to_fahrenheit(celsius)
{:noreply, assign(socket, :fahrenheit, fahrenheit)}
end
Подведение итогов
Сохраняйте логику определения данных как единый источник истины, чтобы избежать головной боли в будущем и трудноисправимых ошибок.
Спасибо всем, и я надеюсь, что вам понравится и вы получите удовольствие. Оставайтесь с нами, чтобы узнать, что будет дальше.
Отдельное спасибо Адольфо Нето, Кристине Гваделупе, Майку Кумму, Виллиану Францу за обзор моей статьи в блоге.