Creating Note taking app using LiveView and GenServer - Part 1

AbulAsar S.AbulAsar S.
9 min read

Phoenix Liveview is a new library that works with Phoenix and provides real-time user experiences with server-rendered HTML. The library documentation itself says LiveView provides rich, real-time user experiences with server-rendered HTML. In this blog, we are going to harness the power and easiness of Liveview and GenServer.

What we are going to build?

We are going to build a simple notetaking app with basic CRUD functionality. Its UI will be similar to the Notes app in Mac OS. I got this inspiration from the Reactjs crash course I did on Udemy. Styling is very much similar to the app in that course. Below screen recording is the entire flow of the project, this will give an idea of the project.

note_app.gif

Why GenServer?

The GenServer is a process that has its own state. We are going to use this state to manage our application state instead of using the database. This is obviously not an ideal scenario to use GenServer, but the purpose of this blog is to introduce LiveView and GenServer usage.

Action plan!!!

This blog is 2 part series.

  • Part 1: We will be generating the application and will add API Layer and Process Layer i.e Genserver (This is Part 1)
  • Part 2: We will build UI using Liveview and integrate the different layers we created in Part 1

Let's start building 👷

  • First of all, our system should have Elixir installed. We can confirm this by running Elixir -v.
  • Once confirmed we need to create/generate Phoenix LiveView application.
  • This can be done by running mix phx.new note_app --no-ecto --live
  • if you get this error
    ** (Mix) The task "phx.new" could not be found
    Note no mix.exs was found in the current directory
    
    Then run mix archive.install hex phx_new
  • Then navigate to project directory cd note_app and run mix phx.server. Once, the project starts running navigate to localhost:4000 to see the welcome phoenix screen.

Creating GenServer!!

  • As promised in the introduction of this blog, we are going to create a Process Layer by using GenServer in it.
  • Create a file note_server.ex inside lib/note_app/notes.
  • Inside the note_server.ex file add the following code.
    defmodule NoteApp.Notes.NoteServer do
     use GenServer
    end
    
  • use GenServer is a macro that adds GenServer behavior in this file.
  • The documentation says GenServer behavior abstracts the common client-server interaction. Developers are only required to implement the callbacks and functionality they are interested in.
  • We are going to implement these callbacks and functionality. That will be our four basic CRUD operations on the note.
    def all_notes()     # To list all notes
    def create_note(note)   # To create note and will take struct as a parameter
    def get_note(id)            # To get particular note
    def delete_note(id)       # To delete particular note
    def update_note(note) #To update note and will take struct as a parameter
    

Inititalizing GenServer

  • The GenServer has the function start_link which is used to start the process. We are going to add our ownstart_link function
  • This function will start the process by calling the GenServer.start_link() function which takes some argument.
  • We will not be passing any parameter so we will ignore the args parameter.
    def start_link(_args) do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
    end
    
  • Genserver#start_link function takes 3 parameters.
    • module name here in this case __MODULE__ gives the name of the module i.e NoteApp.Notes.NoteServer
    • Empty list [] since initially our note will have no notes hence empty list.
    • It takes a list of options, we are passing name.
  • As soon as the start_link function is called it calls init function as a part of the callback call. Let's add it.
    def init(notes) do
      {:ok, notes}
    end
    
  • It has one parameter it is nothing but the initial empty list we created. This callback return :ok tuple with this notes list.
  • We can test this by opening iex shell by running iex -S mix command.
    {:ok, pid} = NoteApp.Notes.NoteServer.start_link(nil)
    
  • This is going to return an ok tuple with some pid value. Something like this {:ok, #PID<0.378.0>} with a different number.
  • This PID is nothing but Process Identifier, which confirms that a separate process has been started by GenServer which will have its own state.
  • By pattern matching, we have assigned this process identifier to pid variable which we'll use later
  • If you may remember we initialize our GenServer with an empty list. We can confirm this by running :sys.get_state(pid) and passing pid value to the get_state() function.
  • This will return an empty list i.e []. You can use this function time to time in shell to check the state of the process.

Implementing CRUD functions

  • We have already discussed we are going to have 5 CRUD functions. Now, we are going to implement that.
  • GenServer has two types of functions client-facing functions and server functions.
  • The above CRUD functions we mentioned will be client functions which will be called from the UI of our apps.
  • These functions will eventually call server functions.
  • Both client and server functions are usually defined in the same fine depending upon the complexity of the project.
  • We can call the server by sending two types of messages. The call messages expect a reply from the server and cast messages do not reply.
  • Let's see one example of each.
  • First method that we are going to implement is all_notes.
  • As the name suggests, with all_notes we are expecting some answer from the server. So, we will use call.

    defmodule NoteApp.Notes.NoteServer do
    # alias NoteApp.Notes.Note
    use GenServer
    
    # Client
    def start_link(_args) do
      GenServer.start_link(__MODULE__, [], name: __MODULE__)
    end
    
    def all_notes(pid) do
      GenServer.call(pid, :all_notes)
    end
    
  • Now the line GenServer.call expects a callback where we will be writing our logic to fetch data from the state.
    def handle_call(:all_notes, _from, notes) do
      {:reply, notes, notes}
    end
    
  • As we know, call returns the current state of the process i.e list of notes. Hence, the tuple is :reply.
  • We can test it in the console by calling NoteServer.all_notes(pid). It will return the list, for now, which is empty.
    iex(3)> NoteServer.all_notes(pid)
    #=> []
    
  • Similarly, we can use cast which does not return anything and is asynchronous. We will create the create_note function which takes map as a parameter and creates note asynchronously.

    #Client
    def create_note(pid, note) do
      GenServer.cast(__MODULE__, {:create_note, note})
    end
    
    #Server
    def handle_cast({:create_note, note}, notes) do
      updated_note = add_id(notes, note)
      updates_notes = [updated_note | notes]
      {:noreply, updates_notes}
    end
    
  • In the callback :create_note is the atom to identify the callback with note as the parameter that we got in the client function and the second parameter notes is nothing but the current state of the process i.e list of notes.
  • Since, it is cast hence we return nothing with :noreply tuple.
  • This is how we implement call and cast. Similarly, we are going to implement the rest of the functions.

    defmodule NoteApp.Notes.NoteServer do
    # alias NoteApp.Notes.Note
    use GenServer
    
    # Client
    def start_link(_args) do
      GenServer.start_link(__MODULE__, [], name: __MODULE__)
    end
    
    def all_notes(pid) do
      GenServer.call(__MODULE__, :all_notes)
    end
    
    def create_note(pid, note) do
      GenServer.cast(__MODULE__, {:create_note, note})
    end
    
    def get_note(pid, id) do
      GenServer.call(__MODULE__, {:get_note, id})
    end
    
    def delete_note(pid, id) do
      GenServer.cast(__MODULE__, {:delete_note, id})
    end
    
    def update_note(pid, note) do
      GenServer.cast(__MODULE__, {:update_note, note})
    end
    
    # Server
    @impl true
    def init(notes) do
      {:ok, notes}
    end
    
    @impl true
    def handle_cast({:create_note, note}, notes) do
      updated_note = add_id(notes, note)
      updates_notes = [updated_note | notes]
      {:noreply, updates_notes}
    end
    
    @impl true
    def handle_cast({:delete_note, id}, notes) do
      updated_notes = notes |> Enum.reject(fn note -> Map.get(note, :id) == id end)
      {:noreply, updated_notes}
    end
    
    @impl true
    def handle_cast({:update_note, note}, notes) do
      updated_notes = update_in(notes, [Access.filter(& &1.id == note.id)], fn _ -> note end)
      {:noreply, updated_notes}
    end
    
    @impl true
    def handle_call({:get_note, id}, _from, notes) do
      found_note = notes |> Enum.filter(fn note -> Map.get(note, :id) == id end) |> List.first
      {:reply, found_note, notes}
    end
    
    @impl true
    def handle_call(:all_notes, _from, notes) do
      {:reply, notes, notes}
    end
    
    defp add_id(notes, note) do
      id = (notes |> Kernel.length) + 1
      %{note | id: id}
    end
    end
    

Managing GenServer by Supervisor!!

  • So far, we have added the process layer and tested the API in the iex shell.
  • In real life app we don't want to manually start the GenServer and keep track of the PID and check process health and keep track of whether it is working fine or crashed.
  • We will give this responsibility to Supervisor.
  • We are going to start the GenServer process as soon as our application boots up.
  • For this, we are going to make some changes in code and make entry of our NoteServer module in the application.ex file.
  • Open application.ex file and in function def start add {NoteApp.Notes.NoteServer, nil} after the line NoteAppWeb.Endpoint,
    def start(_type, _args) do
      .......
      .......
      NoteAppWeb.Endpoint,
      {NoteApp.Notes.NoteServer, nil}
    end
    
  • You will notice {NoteApp.Notes.NoteServer, nil} is a tuple where the first element is our GenServer module name and the second parameter is nil. nil is the value we need to pass to the start_link function, we don't need any hence it is nil.
  • We also need to make some changes in the client functions parameter of the GenServer. i.e all_notes(), create_note(note), get_note(id), delete_note(id),update_note(note).
  • In earlier code, we passed pid as our first parameter to these functions but now we can remove that code because our process is identified by module name (Refer start_link function where __MODULE__ is one of the parameters.

Testing

  • Run the server by running iex -S mix phx.server in the terminal. This runs the server as well as the iex Shell.
  • Now try initializing the GenServer by running {:ok, pid} = NoteApp.Notes.NoteServer.start_link(nil). You will get an error in the console.
    ** (MatchError) no match of right hand side value: {:error, {:already_started, #PID<0.502.0>}}
    
  • We are not sure is our GenServer really running. What is the pid of the process? What would be its current state right now? Let's find out.
  • Lets run pid = Process.whereis(NoteApp.Notes.NoteServer) in the terminal. It is going to return the pid of our GenServer which was initalized as soon as app starts running.
  • We can use this pid to get the latest state of our application in the shell by running :sys.get_state(pid). It will return an empty list. Since we haven't played with the app yet.
  • You can play with different functions in the API layer and check the state at every step to test the application.

This is the end of this blog and part 1. Here we have just created the API layer of the code and taken care of our GenServer which is going to handle the state of our project. In next part we will integrate this API with LiveView and will also create the UI and complete the app.

I hope you like this blog.If you have any questions then please comment below.

References:

12
Subscribe to my newsletter

Read articles from AbulAsar S. directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

AbulAsar S.
AbulAsar S.

I am a Software Engineer from Mumbai, India. In love with Functional programming ( precisely Elixir). Love to share the knowledge that I learn while developing things.