Fork me on GitHub

Server-Side Rendering with Elixir/ElixirScript

An experiment on running ElixirScripts directly on the server to render HTML pages

When implementing the Elxir Todo App and thinking about how to test the ElixirScript client code, one idea was to use an Elixir testing framework. Since the client code is also valid Elixir code, we could directly test it on the Beam VM instead of first translating it into JavaScript and then testing it in a Node.js environment.

Taking this idea one step further: Can we get server-side rendering without the need to run JavaScript code on our server and instead execute the client-side code directly inside our Phoenix app?

To some extent, the answer is: yes we can! Let us look at how to run the ElixirScript code inside a Phoenix controller to generate the initial HTML page.

All of the code below can be found in the server-side-rendering branch of the GitHub Repository for the Elixir Todo App. In addition to the requirements listed in the Install and run the Elixir Todo App section of the Elxir Todo App tutorial, you also need to install the Yarn package manager, because this branch depends on the GitHub version of ElixirScript.

Execute the browser client code inside your Phoenix app

We don't need to modify anything to be able to access the ElixirScript modules in our server code. Since the web directory is added to the elixirc_paths list, this also includes all modules in the web/static/exjs directory. To prevent naming collisions we should put all client modules into a special namespace, for example Client. In the application controller that serves the client app, we can no run the same code as in the client to render the initial page:

defmodule ElixirTodoApp.AppController do
  def index conn, _params do
    todos = Todo |> Todo.latest_first |> Repo.all |> Enum.map(&TodoView.todo_to_json/1)
    store = Client.create_store()
      |> Client.add_reducers()
      |> Client.Store.dispatch({:load_todos, todos})
    render conn, "index.html", dom: render_page(store)
  end

  def render_page store do
    store |> Client.Store.state |> Client.Views.page
  end
end

We first retrieve all Todos from the database. Then we create the initial store of the client's application state, next we register the reducers and finally we dispatch the action {:load_todos, todos} which populates the todo items into the store in the same way the client would do. Note that we don't register any middlewares or subscribers since we don't need nor want their side effects to be executed on the server. Eventually the render_page() function transforms the resulting store into a view that can be rendered in an eex Template.

Stub the React.createElement() function so that it generates Phoenix.HTML.Tags

If we execute this code without any additional modifications, it would fail because no React module is defined on the server (it is a JavaScript module). We need to provide our own implementation that generates some kind of HTML structure.

The details are a little bit unpleasant, because the React.createElement() function takes a variable number of child elements and you cannot pass it a child array. Therefore i've created a new (ElixirScript) function that takes an array of child elements:

React.createElementArray = function(tag, attributes, children) {
  return React.createElement(tag, attributes, ...children);
};

and the ReactUI macros now use unquote instead of unquote_splicing:

defmacro unquote(tag)(attrs, do: inner) do
  tag = Atom.to_string(unquote(tag))
  { inner, attributes } = do_tag(inner, attrs)
  quote do
    React.createElementArray(unquote(tag), unquote(attributes), unquote(inner))
  end
end

On the server side we cleanup the element attributes (for example convert className to class) and use Phoenix.HTML.Tag.content_tag() to create an HTML DOM.

defmodule React do
  def createElementArray tag, attributes, children do
    Phoenix.HTML.Tag.content_tag String.to_atom(tag), children,
      cleanup_attributes(attributes)
  end

  def cleanup_attributes attributes do
    attributes
    |> replace_attribute("className", "class")
    |> delete_attribute("onChange")
    |> delete_attribute("onKeyUp")
    |> delete_attribute("onClick")
    |> to_keyword_list()
  end

  defp replace_attribute attributes, key, new_key, fun \\ &(&1) do
    case Map.get attributes, key do
      nil -> attributes
      value -> attributes
        |> Map.delete(key)
        |> Map.put(new_key, fun.(value))
    end
  end

  defp delete_attribute attributes, key do
    Map.delete attributes, key
  end

  defp to_keyword_list(dict) do
    Enum.map(dict, fn({key, value}) -> {String.to_atom(key), value} end)
  end
end

Now that we have a Phoenix.HTML.Tag structure, we can render it in our app/index.html.eex template inside the application div:

<div id="app">
  <%= @dom %>
</div>

Since we've removed all event listeners from the elements and haven't sent any JavaScript to the client yet, at this point the application does nothing. But as soon as the JavaScript is received, it executes and re-renders the page with interactive elements.

Send the initial application state along with the HTML page

Currently our client app starts with some predefined state, i.e. an empty todo list, and it dispatches an initial action to load the todo items from the server. We're already displaying the todo items with the initial response from the server, so this step is at least unnecessary. But in most cases it is plain wrong. We could have requested any client state by passing a hash route (implementing server side hash-routing is not part of this tutorial) to the server and then the displayed state doesn't match the hard coded initial state. We need a way to transfer the state to the client.

In our controller, we've already calculated this application state

def index conn, _params do
  todos = Todo |> Todo.latest_first |> Repo.all |> Enum.map(&TodoView.todo_to_json/1)
  store = Client.create_store()
    |> Client.add_reducers()
    |> Client.Store.dispatch({:load_todos, todos})
  render conn, "index.html",
    dom: render_page(store),
    state: Client.Store.state(store)
end

and can pass it as a JSON object to the client in the app/index.html.eex template:

<div id="app">
  <%= @dom %>
</div>
<script type="application/json" id="state"%>
  <%= raw Poison.encode!(@state) %%>
</script%>

On the client, we parse the JSON and put it into the store as the initial state:

defmodule Client do
  def create_store do
    initial_state = %{
      todos: [],
      new_todo_text: "",
      edit_todo: nil,
      hide_done: false,
      tutor: :done,
      config: %{}
    }
    json_state = JSON.parse(:document.getElementById("state").innerHTML)
    Store.new(case json_state do
      nil -> initial_state
      json -> Map.merge initial_state, %{
        todos: Enum.map(json_state["todos"], &(Todo.new_from_json &1)),
        config: JSON.parse :document.getElementById("config").innerHTML
      }
    end)
  end
end

Once again we need to ensure that this code can run on the client as well as on the server, therefore we check if there is any initial server state at all. And for this to work, we also need to stub some JavaScript functions:

defmodule JSON do
  def parse(s), do: Poison.decode! s
end

defmodule :document do
  def getElementById("state"), do: %{ innerHTML: "null" }
end

We also don't want to dispatch the initial action that loads the todo items. If this was the only action, we need to replace it with a dummy action, so that the subscribers are called to re-render the page.

To sum it up: We can access our application and the server sends us an initial HTML page (try to load this page with httpie or some similar HTTP client to confirm that you can see the todo items even if no JavaScript is available). The initial application state is contained in this page and as soon as the Client.start() function executes, our application is fully functional.

The downsides of this approach

The advantage of this approach is that we don't need to execute any JavaScript on our server. The downside is that we don't execute any JavaScript on our server. So if we include any React components into our app, they won't get rendered on the server-side. Only native HTML elements are automatically handled by this rendering process.

We could work around this problem by generating appropriate HTML for these components on our own (in the React.createElementArray() function). But this would mean to recreate a lot of the code from these components.

So i conclude that my proposed method of using ElixirScript for server-side rendering is only feasible, if you can mostly rely on using native HTML elements.