Stop mixing LiveView handlers with JavaScript hooks

almirsarajcic

almirsarajcic

9 hours ago

Handling the same event in both Phoenix and JavaScript creates race conditions and unpredictable behavior. Keep server logic in LiveView handlers and client-only interactions in JavaScript hooks.

# ❌ RACE CONDITION - Both server and client handle the same action
defmodule SearchLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    {:ok, assign(socket, results: [], query: "")}
  end

  def handle_event("search", %{"query" => query}, socket) do
    results = perform_search(query)
    {:noreply, assign(socket, results: results, query: query)}
  end

  # Server clearing conflicts with JavaScript clearing
  def handle_event("clear_search", _params, socket) do
    {:noreply, assign(socket, results: [], query: "")}
  end
end
// JavaScript ALSO tries to clear - creates race condition
const SearchHook = {
  mounted() {
    this.el.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') {
        this.el.value = '' // Client clears input
        this.pushEvent('clear_search') // Server ALSO clears
      }
    })
  },
}

The problem: pressing Escape triggers client-side clearing AND server-side clearing. The input flashes empty, then re-renders, creating visual glitches and wasted round trips.

# ✅ CLEAN SEPARATION - Server handles data, client handles UI
defmodule SearchLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    {:ok, assign(socket, results: [], query: "")}
  end

  # Server only: Handle search logic
  def handle_event("search", %{"query" => query}, socket) do
    results = perform_search(query)
    {:noreply, assign(socket, results: results, query: query)}
  end
end
// Client only: Handle instant UI feedback
const SearchHook = {
  mounted() {
    this.el.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') {
        this.el.value = ''
        this.pushEvent('search', { query: '' })
      }
    })
  },
}

export default SearchHook

Now pressing Escape clears the input instantly (JavaScript) and notifies the server (LiveView event). No race condition, no double-handling.

When to use JavaScript hooks vs LiveView handlers

JavaScript hooks (client-only):

  • Instant UI feedback (focus, blur, animations)
  • Keyboard shortcuts (Escape, Ctrl+K)
  • DOM manipulation that doesn’t need server knowledge
  • Browser APIs (clipboard, local storage, geolocation)

LiveView handlers (server-only):

  • Database queries
  • Business logic
  • Authorization checks
  • State management that other LiveViews need to know about

Both together:

  • JS hook handles instant client feedback → pushEvent() → LiveView handler processes server logic

This pattern eliminates race conditions and gives you instant client feedback with reliable server processing.