Navigation
Navigating to the same route
Read time: 8 minutes
This guide is a direct continuation of the previous guide
git clone https://github.com/adopt-liveview/v2-myapp.git --branch query-string-done-done.
Sometimes it can be useful for a LiveView to be used on more than one route. Let's recap the route system made in a previous lesson:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def mount(_params, _session, socket) do
socket = assign(socket, tab: "home")
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<%= case @tab do %>
<% "home" -> %>
<p>You're on my personal page!</p>
<% "about" -> %>
<p>Hi, I'm a LiveView developer!</p>
<% "contact" -> %>
<p>Mail me to bot [at] company [dot] com</p>
<% end %>
</div>
<input disabled={@tab == "home"} type="button" value="Open Home" phx-click="show_home" />
<input disabled={@tab == "about"} type="button" value="Open About" phx-click="show_about" />
<input disabled={@tab == "contact"} type="button" value="Open Contact" phx-click="show_contact" />
<input disabled={@tab == "blog"} type="button" value="Open Blog" phx-click="show_blog" />
"""
end
def handle_event("show_" <> tab, _params, socket) do
socket = assign(socket, tab: tab)
{:noreply, socket}
end
end
Despite being simple and working correctly, this system had a UX problem: if you restart the page you will go back to the home tab. We can solve this by saving the current tab in the URL. If the page is refreshed, we can read the URL and apply the current tab. Update router.ex like this:
scope "/", MyappWeb do
pipe_through :browser
live "/tab/:tab", PageLive, :show
live "/other", OtherPageLive, :other
live "/blog/:slug", BlogLive, :index
end
Then also update PageLive:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def mount(%{"tab" => tab}, _session, socket) do
socket = assign(socket, tab: tab)
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<%= case @tab do %>
<% "home" -> %>
<p>You're on my personal page!</p>
<% "about" -> %>
<p>Hi, I'm a LiveView developer!</p>
<% "contact" -> %>
<p>Mail me to bot [at] company [dot] com</p>
<% end %>
</div>
<.link :if={@tab != "home"} navigate={~p"/tab/home"}>Go to home</.link>
<.link :if={@tab != "about"} navigate={~p"/tab/about"}>Go to about</.link>
<.link :if={@tab != "contact"} navigate={~p"/tab/contact"}>Go to contact</.link>
"""
end
end
To be able to add parameters to our route, we once again created a live route that maps /tab/:tab to our LiveView PageLive. Visit http://localhost:4000/tab/home to see your application. It's worth mentioning that we used Live Action :show this time, as we are showing a single item in each tab.
As we are now working with routes, the buttons were replaced by <.link> components. Our mount/3 receives the initial value from the params tab.
#Optional route parameter
You may have noticed that we create a bad experience for new users, as the home page does not exist and the user is forced to type /tab/home. We can solve this by letting our mount/3 handle the tab param in a different way and also making a new route. Update your router.ex:
scope "/", MyappWeb do
pipe_through :browser
live "/", PageLive, :show
live "/tab/:tab", PageLive, :show
live "/other", OtherPageLive, :other
live "/blog/:slug", BlogLive, :index
end
And your PageLive like so:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def mount(params, _session, socket) do
tab = params["tab"] || "home"
socket = assign(socket, tab: tab)
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<%= case @tab do %>
<% "home" -> %>
<p>You're on my personal page!</p>
<% "about" -> %>
<p>Hi, I'm a LiveView developer!</p>
<% "contact" -> %>
<p>Mail me to bot [at] company [dot] com</p>
<% end %>
</div>
<.link :if={@tab != "home"} navigate={~p"/"}>Go to home</.link>
<.link :if={@tab != "about"} navigate={~p"/tab/about"}>Go to about</.link>
<.link :if={@tab != "contact"} navigate={~p"/tab/contact"}>Go to contact</.link>
"""
end
end
We just add a new route using the same LiveView and change the way we handle the params, then our PageLive becomes capable of being used in a context with or without a route parameter! It is worth noting that we modified our <.link> from Home to send to /; however, /tab/home also works normally.
#Optimizing navigation in the same LiveView
When you use <.link navigate={...}>, LiveView understands that you are changing from one LiveView to a different one and need to create a new context. If you know beforehand that a transition goes to the same LiveView, you can use the alternative <.link patch={...}> and the modification between routes will be even more optimized. For this to work correctly, we need to introduce a new callback called handle_params/3. Update your PageLive file:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def mount(params, _session, socket) do
tab = params["tab"] || "home"
socket = assign(socket, tab: tab)
{:ok, socket}
end
def handle_params(params, _uri, socket) do
tab = params["tab"] || "home"
socket = assign(socket, tab: tab)
{:noreply, socket}
end
def render(assigns) do
~H"""
<div>
<%= case @tab do %>
<% "home" -> %>
<p>You're on my personal page!</p>
<% "about" -> %>
<p>Hi, I'm a LiveView developer!</p>
<% "contact" -> %>
<p>Mail me to bot [at] company [dot] com</p>
<% end %>
</div>
<.link :if={@tab != "home"} patch={~p"/"}>Go to home</.link>
<.link :if={@tab != "about"} patch={~p"/tab/about"}>Go to about</.link>
<.link :if={@tab != "contact"} patch={~p"/tab/contact"}>Go to contact</.link>
"""
end
end
The handle_params/3 callback is very similar to mount/3, except that the second argument contains the URI of the current page and the return must be {:noreply, socket}.
One annoying thing at the moment is the fact that we have duplicated code between our mount/3 and handle_params/3. Fortunately, there is a very simple solution for this. Whenever a LiveView is initialized by Phoenix for the first time, it executes mount/3 if it exists and then handle_params/3 if it exists. This way, we can remove mount/3 completely. Update PageLive:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def handle_params(params, _uri, socket) do
tab = params["tab"] || "home"
socket = assign(socket, tab: tab)
{:noreply, socket}
end
def render(assigns) do
~H"""
<div>
<%= case @tab do %>
<% "home" -> %>
<p>You're on my personal page!</p>
<% "about" -> %>
<p>Hi, I'm a LiveView developer!</p>
<% "contact" -> %>
<p>Mail me to bot [at] company [dot] com</p>
<% end %>
</div>
<.link :if={@tab != "home"} patch={~p"/"}>Go to home</.link>
<.link :if={@tab != "about"} patch={~p"/tab/about"}>Go to about</.link>
<.link :if={@tab != "contact"} patch={~p"/tab/contact"}>Go to contact</.link>
"""
end
end
Now we can optimize the navigation between the same LiveView by simply making links use the patch attribute and changing from mount/3 to handle_params/3.
Should I optimize all routes?
navigate in all your .link components.
#Recap!
- A LiveView can be used on more than one route.
- We can take advantage of URLs to persist data in cases such as tabs.
-
handle_params/3is a callback that is executed right aftermount/3. -
One way to optimize page changes for the same LiveView is to use
patchin the<.link>components. -
Using
patchwe executehandle_params/3.
Feedback
Got any feedback about this page? Let us know!