Единый источник истины с Phoenix LiveView

Я работал с 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
Войти в полноэкранный режим Выход из полноэкранного режима

Подведение итогов

Сохраняйте логику определения данных как единый источник истины, чтобы избежать головной боли в будущем и трудноисправимых ошибок.

Спасибо всем, и я надеюсь, что вам понравится и вы получите удовольствие. Оставайтесь с нами, чтобы узнать, что будет дальше.

Отдельное спасибо Адольфо Нето, Кристине Гваделупе, Майку Кумму, Виллиану Францу за обзор моей статьи в блоге.

Оцените статью
devanswers.ru
Добавить комментарий