Written using Phoenix 1.7.2 and Phoenix LiveView 0.18.18
Every now and then in my daily development work, I find something within Phoenix and LiveView that surprises me a little bit, that requires a bit of thought and tinkering to make it work the way I would expect.
Today’s topic: flash messages within LiveView components.
Most of us are probably familiar with flash messages - the little (usually coloured) boxes that pop up to let us know pertinent information that we might find interesting, usually related to what we’re doing right now.
How Phoenix presents flash messages, with default styling from Phoenix 1.7.
On the web they’re usually presented after a state change, page redirect, etc. If you submit a form to edit a blog post, you’ll typically be redirected to a different page and then the flash message will display.
But Phoenix, LiveView and its component system for rendering pages make this a little bit different.
Flash messages and components#
In Phoenix, flash messages are typically at the top page level, for the whole page - LiveView makes a @flash
assign available to your liveviews, and for use in your templates. The default application layout in app.html.heex
uses this assign to render nicely formatted messages, as seen above.
But every child component rendered on the page also has its own flash data, and the two don’t overlap.
See the code behind this video
Only flash messages set in the top-level LiveView are accessible.
Conceptually, this kind of makes sense. The way you might show a flash after taking some small action such as liking a post, might be totally different than a flash message you show for something big like editing a post. But in practice, a lot of the time you’ll want to use the same display method for flashes coming from anywhere. (A flash message from a form submission shouldn’t render differently just because you moved the form into a component!) So what can you do about it?
Flash messages via message sending#
Elixir already has a native way of sending messages around, and we can use it here to send a flash message from a component to its parent liveview for rendering.
From the component#
In LiveView, only liveviews themselves run in their own server processes - all live components run in the same process as the liveview they’re rendered within. We can take advantage of this and use send/2
from the component, and send a message to self()
(which is the LiveView process).
To make this new put_flash!/3
function accessible to all components, you can import it into live_component/0
in your web.ex
- this is what gets called when you call use MyAppWeb, :live_component
.
In your components, you can now use put_flash!/3
instead of put_flash/3
, and you’ll get the new message-sending behaviour instead of setting the flash message only within the component itself.
To the LiveView#
Sending messages is all well and good, but they need to be received to be of any use!
To receive a message, you need a handle_info/3
function definition, the first argument being the message received. If you wanted to add this to a single liveview, you could write it like so:
This takes the flash out of the message sent from the component, and copies it into the flash for the liveview so it gets treated like any other flash message. Awesome! But making this function available to all liveviews is a little bit trickier.
handle_info/3
(and other GenServer callbacks) are a bit special in that they get called dynamically via apply/3
internally, so importing them won’t behave as expected. Importing a function into a module doesn’t actually add the imported functions into the module - it only makes them callable without needing to specify the module name. You can verify this in iex
:
So adding the handle_info/3
function to our Flash module and importing it in web.ex
, like we did with put_flash!/3
, won’t work.
What we can do, however, is hook into LiveView’s lifecycle to add extra functions to liveviews on mount. This uses on_mount/1
to add the hook, and then in the hook, use attach_hook/4
to add the handle_info/3
function definition. (It sounds more complicated than it is!)
In code, it looks like this (using the same MyAppWeb.Flash
module to keep everything together):
- The
on_mount/1
function call inlive_view/0
will callon_mount/4
in the named module (in our case,MyAppWeb.Flash
) - The
on_mount/4
function inMyAppWeb.Flash
sets up a newhandle_info
callback, pointing it to the newmaybe_receive_flash/2
function - ⚠️ This means that
maybe_receive_flash/2
will be called every timehandle_info
is called! ⚠️ - Because we’re only interested in one type of message -
put_flash
messages - we can accept those and handle them, setting the flash and returning{:halt, updated_socket}
(so further callbacks won’t run). - Any other message, we just pass on to the rest of the callback chain.
And by including this into all liveviews in web.ex
, any component that calls put_flash!/3
will have the flash handled by its parent liveview and handled appropriately.
See the code behind this video
(I’ve set up a GitHub repository with some proof-of-concept code getting this working - you can check it out here.)
This technique can be used for a lot of interesting things - in the LiveBeats app it gets used for loading data, setting event handlers, and also handling params across all liveviews.
What else will you use it for?
Special thanks to Chris McCord for pointing me in the right direction!