Inertia.js Phoenix Adapter

repository·main·Indexed 19 days ago

https://github.com/inertiajs/inertia-phoenix

The Inertia.js Phoenix Adapter allows developers to build single-page applications using Elixir/Phoenix on the backend and React, Vue, or Svelte on the frontend. It maintains a classic server-side routing model and provides helpers for rendering Inertia responses, managing props with lazy and deferred evaluation, handling Ecto.Changeset validation errors, and implementing infinite scroll.

Tokens
12.9K
Snippets
49
Records
54
Agent score
66%

What's inside inertia-phoenix

  1. Access Phoenix flash messages in Inertia props

    main

    The library automatically maps Phoenix flash data to the flash key within your Inertia props. When you use put_flash/3 in a Phoenix controller and redirect, the flash messages will be available in the frontend component's props under flash.

    Example flow:

    1. Controller calls conn |> put_flash(:info, "message") |> redirect(...).
    2. Frontend receives props: { "props": { "flash": { "info": "message" } } }.
    def update(conn, params) do
      case MyApp.Settings.update(params) do
        {:ok, _settings} ->
          conn
          |> put_flash(:info, "Settings updated")
          |> redirect(to: ~p"/settings")
    
        {:error, changeset} ->
          conn
          |> assign_errors(changeset)
          |> redirect(to: ~p"/settings")
      end
    end
  2. Use lazy data evaluation for expensive props

    main

    To avoid expensive computations during every request, especially when using partial reloads, you can pass a function reference to assign_prop/2.

    • Standard Lazy Prop: Passing an anonymous function fn -> ... end or a named function reference ensures the computation is only performed when the prop is actually needed. It is included on the first visit.
    • Optional Lazy Prop: Wrapping a function with Inertia.Controller.inertia_optional/1 ensures the prop is never included on the first visit; it will only be included if explicitly requested during a partial reload.
    # ALWAYS included on first visit, but ONLY evaluated when needed
    |> assign_prop(:expensive_thing, fn -> calculate_thing() end)
    
    # NEVER included on first visit, ONLY evaluated when requested in a partial reload
    |> assign_prop(:super_expensive_thing, inertia_optional(fn -> calculate_thing() end))
  3. Install the Inertia.js Phoenix Adapter using Igniter

    main

    The easiest way to install the adapter is using Igniter. This automates the setup process and allows for customization via CLI flags.

    Installation Commands

    mix archive.install hex igniter_new
    mix igniter.install inertia

    Customization Options

    Use these flags with the mix igniter.install inertia command:

    • --client-framework [react|vue|svelte]: Configures the client-side framework in assets/package.json.
    • --camelize-props: Sets camelize_props: true in config.exs.
    • --history-encrypt: Sets history: [encrypt: true] in config.exs.
    • --typescript: Creates a TypeScript config file and installs dev dependencies.
  4. Configure esbuild for SSR bundling

    main

    Update your config/config.exs to include an ssr build configuration for esbuild. This configuration should target node, use the cjs format, and output to the priv directory.

    # config/config.exs
    
    config :esbuild,
      version: "0.21.5",
      app: [
        args: ~w(js/app.jsx --bundle --target=es2020 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
        cd: Path.expand("../assets", __DIR__),
        env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
      ],
      ssr: [
        args: ~w(js/ssr.jsx --bundle --platform=node --outdir=../priv --format=cjs),
        cd: Path.expand("../assets", __DIR__),
        env: %{"NODE_PATH" => Path.expand("../deps", __DIR")}
      ]
  5. Set up the Inertia.js client-side (React example)

    main

    To boot the Inertia app on the client side, install the Inertia library for your framework (e.g., @inertiajs/react) and configure your entrypoint (e.g., assets/js/app.jsx).

    Client-side Bootstrapping

    // assets/js/app.jsx
    import React from "react";
    import axios from "axios";
    import { createInertiaApp } from "@inertiajs/react";
    import { createRoot } from "react-dom/client";
    
    axios.defaults.xsrfHeaderName = "x-csrf-token";
    
    createInertiaApp({
      resolve: async (name) => {
        return await import(`./pages/${name}.jsx`);
      },
      setup({ App, el, props }) {
        createRoot(el).render(<App {...props} />);
      },
    });

    Esbuild Configuration

    Ensure your esbuild configuration meets these requirements:

    • version: >= 0.19.0 (for glob-style imports).
    • args: The entrypoint should use the correct extension (e.g., .jsx) and the --target should be at least es2020.

    Enabling ESM Code Splitting

    To enable code splitting with esbuild, update your config.exs with the following flags:

    • --splitting
    • --format=esm
    • --chunk-names=chunks/[name]-[hash]

    Note: ESM code splitting requires updating your root layout to load the script as a module:

    <script type='module' defer phx-track-static src={~p"/assets/app.js"}></script>
    // assets/js/app.jsx
    import React from "react";
    import axios from "axios";
    
    import { createInertiaApp } from "@inertiajs/react";
    import { createRoot } from "react-dom/client";
    
    axios.defaults.xsrfHeaderName = "x-csrf-token";
    
    createInertiaApp({
      resolve: async (name) => {
        return await import(`./pages/${name}.jsx`);
      },
      setup({ App, el, props }) {
        createRoot(el).render(<App {...props} />);
      },
    });
  6. Integrate Inertia.js into Phoenix Controller and HTML helpers

    main

    To use Inertia's rendering capabilities, you must import its helper modules into your application's web module (usually lib/my_app_web.ex).

    1. Import Inertia.Controller in your controller function to access render_inertia/3 and assign_prop/3.
    2. Import Inertia.HTML in your html function to access Inertia components.
    3. Add Inertia.Plug to your browser pipeline in lib/my_app_web/router.ex.
    4. Update your layout to use <.inertia_title> and <.inertia_head>.
      # lib/my_app_web.ex
      defmodule MyAppWeb do
        def controller do
          quote do
            use Phoenix.Controller, namespace: MyAppWeb
    +       import Inertia.Controller
          end
        end
    
        def html do
          quote do
            use Phoenix.Component
    +       import Inertia.HTML
          end
        end
      end
    
      # lib/my_app_web/router.ex
      defmodule MyAppWeb.Router do
        use MyAppWeb, :router
    
        pipeline :browser do
          plug :accepts, ["html"]
    +     plug Inertia.Plug
        end
      end
    
      # lib/my_app_web/components/layouts/root.html.heex
      <!DOCTYPE html>
      <html>
        <head>
    -     <.live_title>{@page_title}</.live_title>
    +     <.inertia_title>{@page_title}</.inertia_title>
    +     <.inertia_head content={@inertia_head} />
        </head>
      </html>
  7. Share data on every request using Plugs

    main

    To share data (like the authenticated user) on every single Inertia request, use assign_prop/2 within a Phoenix Plug in your response pipeline. This ensures the data is serialized and passed to all Inertia components.

    defmodule MyApp.UserAuth do
      import Inertia.Controller
      import Plug.Conn
    
      def authenticate_user(conn, _opts) do
        user = get_user_from_session(conn)
        conn
        |> assign(:user, user) # Store in conn assigns
        |> assign_prop(:user, serialize_user(user)) # Pass to Inertia
      end
    end
  8. Prepare production environment for SSR

    main

    SSR requires Node.js to be installed on your production server. If using Docker, ensure nodejs is installed and set the NODE_ENV environment variable to production. Setting NODE_ENV=production is critical as it ensures the SSR script is cached in memory, preventing slow page rendering times.

    # Example Dockerfile snippet
    FROM ${RUNNER_IMAGE}
    
    RUN apt-get update -y && \
        apt-get install -y libstdc++6 openssl curl libncurses5 locales ca-certificates && \
        apt-get clean && rm -f /var/lib/apt/lists/*_*
    
    RUN curl -fsSL https://deb.nodesource.com/setup_x.x | bash - && \
        apt-get update && \
        apt-get install -y nodejs
    
    ENV MIX_ENV="prod"
    ENV NODE_ENV="production"
  9. Add Inertia.SSR to the application supervision tree

    main

    To manage the Node.js process pool, you must add Inertia.SSR to your application's supervision tree in lib/my_app/application.ex. You must provide the path option pointing to the directory containing your compiled ssr.js file (usually priv).

    # lib/my_app/application.ex
    
    defmodule MyApp.Application do
      use Application
    
      def start(_type, _args) do
        children = [
          # ... other children
          {Inertia.SSR, path: Path.join([Application.app_dir(:my_app), "priv")},
          MyAppWeb.Endpoint,
        ]
        Supervisor.start_link(children, strategy: :one_for_one)
      end
    end
  10. Enable Server-Side Rendering (SSR) in Inertia.js Phoenix

    main

    Inertia.js Phoenix supports Server-Side Rendering (SSR) by spinning up a pool of Node.js process workers managed from your Elixir process tree. This allows the client to hydrate HTML that has been pre-rendered on the server instead of performing the initial DOM rendering.

    To enable SSR, you must:

    1. Create a JavaScript ssr.jsx (or equivalent) module that exports a render function.
    2. Configure esbuild to compile the SSR bundle.
    3. Add the SSR build to your watchers and deployment scripts.
    4. Add Inertia.SSR to your application's supervision tree.
    5. Enable SSR in your Inertia configuration.
    6. Ensure Node.js is installed in your production environment.
    ### 1. Create the SSR module (Example using React)
    
    ```js
    // assets/js/ssr.jsx
    import React from "react";
    import ReactDOMServer from "react-dom/server";
    import { createInertiaApp } from "@inertiajs/react";
    
    export function render(page) {
      return createInertiaApp({
        page,
        render: ReactDOMServer.renderToString,
        resolve: async (name) => {
          return await import(`./pages/${name}.jsx`);
        },
        setup: ({ App, props }) => <App {...props} />,
      });
    }
  11. Configure CSRF protection for Axios

    main

    The library automatically sets the XSRF-TOKEN cookie. To ensure Phoenix can validate requests, you must configure your frontend Axios client to send the CSRF token using the x-csrf-token header name.

    Add this configuration to your main JavaScript entry point (e.g., assets/js/app.js):

    // assets/js/app.js
    import axios from "axios";
    axios.defaults.xsrfHeaderName = "x-csrf-token";
    
    // the rest of your Inertia client code...
  12. Test Inertia responses with Inertia.Testing

    main

    The Inertia.Testing module provides helpers to assert the state of Inertia responses in your Elixir tests.

    Key functions:

    • inertia_component(conn): Returns the name of the Inertia component being rendered.
    • inertia_props(conn): Returns the props passed to the component.
    • inertia_errors(conn): Returns the validation errors passed to the component.
    • redirected_to(conn): Asserts the redirect destination.

    Best Practice: Import Inertia.Testing in your ConnCase module to make these helpers available in all controller tests.

    # In your ConnCase helper
    defmodule MyApp.ConnCase do
      use ExUnit.CaseTemplate
    
      using do
        quote do
          import Inertia.Testing
    
          # ...
        end
      end
    end
    
    # In your test
    import Inertia.Testing
    
    test "renders the home page", %{conn: conn} do
      conn = get("/")
      assert inertia_component(conn) == "Home"
      assert %{user: %{id: 1}} = inertia_props(conn)
    end