Table of Contents
Effective Umbrella Project Structures
When building complex applications with Phoenix, organizing your project structure is crucial for maintainability and scalability. The umbrella project structure is a recommended approach for structuring larger applications that consist of multiple smaller applications or services.
In an umbrella project, you have a top-level application that acts as a container for multiple child applications. Each child application can have its own context, controllers, views, and other components. This modular approach allows you to separate concerns and manage dependencies more effectively.
To create an umbrella project in Phoenix, you can use the mix
command with the new
flag and specify the --umbrella
option. Here's an example:
mix new my_app --umbrella
This will create a new umbrella project with a top-level my_app
application and an apps
directory where you can add your child applications.
Let's say we want to create a blog application with separate modules for authentication, user management, and blog posts. We can create three child applications within our umbrella project:
cd my_app/apps mix phx.new auth --no-ecto mix phx.new users --no-ecto mix phx.new blog --no-ecto
In this example, we used the --no-ecto
flag to exclude Ecto, as we'll cover advanced Ecto techniques in a later section.
Each child application will have its own router, context, controllers, views, and other necessary components. You can also share common code and dependencies between the child applications by adding them to the top-level mix.exs
file.
The umbrella project structure provides several advantages:
1. Modularity: Each child application can be developed, tested, and deployed independently, allowing for easier collaboration and scalability.
2. Separation of concerns: By separating different modules into individual applications, you can maintain a cleaner codebase and easily manage dependencies.
3. Code organization: With the umbrella project structure, it's easier to locate and navigate through the different parts of your application, making maintenance and debugging more efficient.
Related Article: Optimizing Database Queries with Elixir & Phoenix
Example: Adding a new child application
Let's say we want to add a new child application called comments
to our blog umbrella project. We can use the following command:
cd my_app/apps mix phx.new comments --no-ecto
This will create a new child application called comments
within the apps
directory. You can then configure the router, context, controllers, and views for the comments
application as needed.
Example: Sharing code between child applications
To share code between child applications, you can add dependencies and common modules to the top-level mix.exs
file. For example, let's say we want to share a utility module called Blog.Utils
between the users
and blog
applications.
In the top-level mix.exs
file, add the :users
and :blog
applications as dependencies and specify the :only
option to include only the necessary modules:
defp deps do [ {:users, in_umbrella: true, only: [:utils]}, {:blog, in_umbrella: true, only: [:utils]} ] end
Now, you can use the Blog.Utils
module in both the users
and blog
applications.
LiveView Optimizations
Phoenix LiveView is a useful library that allows you to build interactive, real-time web applications using server-rendered HTML. However, as your application grows in complexity and user traffic increases, you may encounter performance issues. Here are some best practices and optimizations to improve the performance of your LiveView applications.
1. Minimize server round-trips: LiveView leverages Phoenix Channels to establish a bidirectional connection between the client and the server. Each time a user interacts with a LiveView component, such as submitting a form or clicking a button, the client sends a message to the server, which in turn updates the LiveView state and renders the changes. To minimize server round-trips, use the phx-debounce
attribute to delay the transmission of user events. By batching multiple events together, you can reduce the number of server requests and improve performance.
<button>Click Me</button>
In this example, the phx-debounce
attribute delays the phx-click
event by 300 milliseconds, allowing time to collect other potential events before making a server request.
2. Use Phoenix PubSub for interactivity: Phoenix PubSub is a publish-subscribe system built into Phoenix that allows you to broadcast events to LiveView processes. Instead of relying solely on server round-trips, you can use PubSub to send real-time updates to connected LiveView components. This can significantly reduce the number of server requests and improve interactivity. For example, you can use PubSub to broadcast updates to all connected users when a new comment is posted.
def handle_event("new_comment", %{"content" => content}, socket) do # Save the comment to the database Phoenix.PubSub.broadcast(MyApp.PubSub, "new_comment", %{ "content" => content }) {:noreply, socket} end
In this example, the handle_event/3
function broadcasts a "new_comment"
event using PubSub. Connected LiveView components can subscribe to this event and update their state accordingly.
3. Optimize rendering with diffs: LiveView uses a virtual DOM diffing algorithm to update the client-side HTML efficiently. By default, LiveView compares the entire rendered HTML to the new HTML after a server update. However, you can optimize this process by using the phx-update
attribute to specify which parts of the HTML to update. This can reduce the amount of DOM manipulation and improve rendering performance.
<div> <!-- Content that doesn't need to be updated --> </div> <div> <!-- Content that should be appended to the existing DOM --> </div> <div> <!-- Content that should replace the existing DOM --> </div>
In this example, the phx-update
attribute is used to specify different update strategies for different parts of the HTML.
Related Article: Building Real-Time Apps with Phoenix Channels & WebSockets
Example: Debouncing user input
To debounce user input in a LiveView component, use the phx-debounce
attribute with the desired delay in milliseconds. For example, let's debounce a text input field to delay the transmission of user input by 500 milliseconds:
In this example, the phx-value
attribute binds the input field to the name
property in the LiveView state. The phx-debounce
attribute delays the transmission of user input by 500 milliseconds.
Example: Broadcasting updates with PubSub
To broadcast updates to connected LiveView components using Phoenix PubSub, you can use the Phoenix.PubSub.broadcast/3
function. For example, let's broadcast a "new_comment"
event with the comment content to all connected users:
def handle_event("new_comment", %{"content" => content}, socket) do # Save the comment to the database Phoenix.PubSub.broadcast(MyApp.PubSub, "new_comment", %{ "content" => content }) {:noreply, socket} end
In this example, the handle_event/3
function broadcasts a "new_comment"
event using the Phoenix.PubSub.broadcast/3
function. The event payload includes the comment content.
Advanced Ecto Techniques
Ecto is the database layer in Phoenix that provides a useful and flexible way to interact with databases. In this section, we'll explore some advanced Ecto techniques that can enhance your application's functionality.
Ecto Multi-Tenancy
Multi-tenancy is a common requirement in applications where multiple clients or organizations share the same software instance but have their own isolated data. Ecto provides several techniques to implement multi-tenancy.
One approach is to use a separate database schema for each tenant. This allows you to isolate the data and apply tenant-specific configurations. To dynamically switch between tenant schemas, you can use Ecto's Repo
module and the Ecto.Schema
macro.
Here's an example of how you can implement multi-tenancy with separate schemas:
defmodule MyApp.Repo do use Ecto.Repo, otp_app: :my_app end defmodule MyApp.TenantRepo do alias MyApp.Repo defdelegate all(tenant), to: Repo, as: :all defdelegate get(tenant, id), to: Repo, as: :get defdelegate insert(tenant, params), to: Repo, as: :insert defp schema_prefix(tenant) do "tenant_#{tenant}_" end defmacro __using__(tenant) do quote do use Ecto.Schema, prefix: unquote(schema_prefix(tenant)) import Ecto.Changeset @tenant unquote(tenant) schema "table_name" do # define your schema fields and associations end def changeset(struct, params \\ %{}) do struct |> cast(params, @required_fields, @optional_fields) |> validate_required(@required_fields) end end end end defmodule MyApp.Tenant do use MyApp.TenantRepo, "my_tenant" # define tenant-specific functions and validations end
In this example, we define a MyApp.TenantRepo
module that delegates database operations to the main MyApp.Repo
module but prefixes the schema with the tenant name. We also define a MyApp.Tenant
module that uses the MyApp.TenantRepo
module and can be used to perform CRUD operations on the tenant-specific schema.
Related Article: Implementing Enterprise Features with Phoenix & Elixir
Ecto Aggregates
Aggregates in Ecto allow you to perform complex queries that involve calculations, grouping, and aggregations on your database. Ecto provides a set of functions and macros to work with aggregates, such as count
, sum
, avg
, group_by
, and more.
Here's an example of how you can use aggregates in Ecto:
defmodule MyApp.User do use Ecto.Schema schema "users" do field :name, :string field :age, :integer timestamps() end end
In this example, we have a users
table with name
and age
columns.
defmodule MyApp.UserStats do alias Ecto.Query def age_stats do from(u in User, select: %{average_age: avg(u.age), min_age: min(u.age), max_age: max(u.age)} ) |> Repo.one() end end
In this example, the age_stats
function calculates the average, minimum, and maximum age of all users in the database using the avg
, min
, and max
aggregate functions. The result is a map with the calculated statistics.
Ecto CTEs
Common Table Expressions (CTEs) allow you to define temporary named result sets within a query and reference them multiple times. This can be useful for complex queries that involve subqueries or recursive queries.
Ecto supports CTEs through its Ecto.Query.API.with_cte/3
function. Here's an example of how you can use CTEs in Ecto:
defmodule MyApp.User do use Ecto.Schema schema "users" do field :name, :string field :manager_id, :integer timestamps() end end
In this example, we have a users
table with name
and manager_id
columns.
defmodule MyApp.UserHierarchy do alias Ecto.Query def manager_subordinates(manager_id) do query = from u in User, with_cte subordinates_query <- from(u in User, where: u.manager_id == ^manager_id), select: u.name, where: u.manager_id == ^manager_id or u.id in subordinates_query Repo.all(query) end end
In this example, the manager_subordinates
function retrieves all users who directly report to a given manager, as well as their subordinates. It uses a CTE named subordinates_query
to define a subquery that retrieves the subordinates of a given manager.
Phoenix LiveView Best Practices
Phoenix LiveView is a useful tool for building real-time, interactive web applications. To make the most out of LiveView, here are some best practices to follow:
1. Keep the LiveView state minimal: The LiveView state represents the current state of the UI on the server. It's important to keep the state minimal to minimize the amount of data that needs to be sent back and forth between the client and the server. Avoid storing unnecessary data in the LiveView state and only include data that is required for rendering and interactivity.
2. Use LiveView hooks for client-side interactivity: LiveView hooks allow you to add client-side interactivity to LiveView components. Hooks are JavaScript functions that run on the client-side and can be used to perform DOM manipulation, event handling, and other client-side operations. Use hooks to enhance the interactivity of your LiveView components while keeping the server-side logic focused on data management.
3. Leverage the phx-change
event for data synchronization: The phx-change
event is triggered when an input field in a LiveView component changes. You can use this event to synchronize the data between the client and the server. For example, you can send the updated field value to the server and update the LiveView state accordingly. This ensures that the server and the client are always in sync.
4. Handle server-side validation and error handling: LiveView provides built-in support for server-side validation and error handling. Instead of relying solely on client-side validation, perform server-side validation to ensure data integrity and security. Use the phx-feedback-for
function to display validation errors and other feedback from the server.
5. Optimize rendering with LiveView assigns: LiveView assigns allow you to pass data from the server to the client for rendering. When updating the LiveView state, only update the necessary assigns to minimize the amount of data that needs to be sent to the client. This can improve rendering performance and reduce network traffic.
6. Use phx-throttle
to limit server requests: The phx-throttle
attribute allows you to limit the number of server requests sent by LiveView components. By setting a throttle interval, you can prevent excessive server requests caused by rapid user interactions. This can help optimize performance and prevent unnecessary server load.
7. Test LiveView components thoroughly: LiveView components should be tested thoroughly to ensure their correctness and reliability. Use the built-in testing utilities provided by Phoenix to write tests for your LiveView components. Test both the server-side logic and the client-side interactivity to cover all aspects of your LiveView components.
Example: Using a LiveView hook for client-side interactivity
To add client-side interactivity to a LiveView component, you can use a LiveView hook. Here's an example of a hook that adds a click event listener to a button in a LiveView component:
// assets/js/app.js import { LiveSocket } from "phoenix_live_view"; let liveSocket = new LiveSocket("/live"); liveSocket.connect(); let Hooks = {}; Hooks.ClickButton = { mounted() { let button = this.el.querySelector("button"); button.addEventListener("click", () => { // Perform client-side logic }); } }; liveSocket.hook(Hooks);
In this example, the ClickButton
hook adds a click event listener to the button element in the LiveView component. When the button is clicked, the client-side logic inside the event listener is executed.
To use the hook in your LiveView component, add the phx-hook
attribute to the button element:
<button>Click Me</button>
Related Article: Elixir’s Phoenix Security: Token Auth & CSRF Prevention
Example: Handling server-side validation and error handling
To handle server-side validation and error handling in a LiveView component, you can use the phx-feedback-for
function. Here's an example:
<ul> <%= for {field, message} <li phx-feedback-for=""></li> </ul>
In this example, if @errors
is not empty, a list of error messages is rendered. Each error message is associated with a specific field using the phx-feedback-for
function. This allows LiveView to display the error message next to the corresponding field.
To trigger server-side validation and error handling, you can use the phx-change
event in an input field:
<input type="text" phx-change="validateField" phx-value="">
In this example, the phx-change
event is triggered when the input field changes. The validateField
function in the LiveView handles the event and performs server-side validation. The error messages are then assigned to the @errors
assign for rendering.