blog.brng.us

    Wiring reflux and react to phoenix websockets

    Phoenix Sockets and react.js components via reflux.js

    In this post I will describe how to wire up some server state to our react components

    Environment

    This quick tutorial has only been tested with the following component versions. Beware trying to get this working with other versions. Phoenix is not yet 1.0 and it may change enough to make this no longer work.

    Git Repo: [https://github.com/jschoch/reflux_eventbroker_react_phoenix_elixir/tree/logs]

    Elixir 1.05 and Phoenix 0.16.1 components:

    Bower components for reflux 0.2.7, react 0.13.3

    How

    The first post described how to use react.js and reflux to coordinate react components. This post will describe how to wire in server state through phoenix sockets.

    The great thing about this approach is that you largely remove the drudgery of creating a REST CRUD api, and just send json down to your javascript as events. The dataflow is in one direction, which simplifies our design.

    To demonstrate this we will be adding a few key components. The first will be an Elixir Agent to hold our state. The second will be to integrate our Reflux store with Phoenix sockets. We will track clicks to our buttons, and who’s using the system.

    reflow shows the basic flow of data.

    Let's look at how to hold our server state.

    An elixir agent is essentially a server process that will hold state in memory. It can leverage supervision, and be run as an application. The Elixir docs for Agent can be found here. Our agent will provide functions to:

    • Register a user login
    • Register a user logout
    • Register a user click
    • Retrieve the current user_count
    • Retrieve the current state
    • Start and Stop

    I also subbed out broadcasting and messages, which we will ignore for now.

    lib/agent.ex

    setup state: def start_link

    our start_link function initializes our state. In this case it is simply a map with default attributes: an empty list for users, a zero user count, an empty list for messages, and a zero count for hits.

    We simply pass a function which returns the map, and a name, which gets compiled to the module name Elixir.LogAgent or in Elixir just LogAgent.

    def start_link do
        map = %{users: [],user_count: 0, msgs: [],hits: 0} 
        Agent.start_link(fn -> map end, name: __MODULE__)
      end

    get state: def get

    Def simply calls Agent.get with our module name, and it uses function shorthand, or partial application to return the state.

    def get_user_count do
        Agent.get(__MODULE__,&Map.fetch(&1,:user_count))
      end

    We could also write this more verbose

    def get do
        Agent.get(__MODULE__,fn(state) -> 
          # last arg is returned
          state
        end)
      end

    register a login: def login(user)

    This grabs a user and puts it into our state map. One flaw to be fixed here is the fact that we don’t check to see if the user is already logged in. I

    If you are unfamiliar with Elixir or Erlang, the syntax for adding a user to our list may be confusing. This is called a “cons cell”, and it allows you to reference a list as a head and a tail. When used on the left side of “=” it interpolates the first element of a list into the variable on the left of the “|”, and the rest of the list to the right.

    [head|tail] = [1,2,3,4,5]

    head is now 1, and tail is now [2,3,4,5]. this is because “=” is not an assignment operator like most languages, but a pattern match.

    When used on the right side of “=”, or bare you prepend head to your list.

    # bare
    iex(7)> element = 1
    1
    iex(8)> list = [2,3,4]
    [2, 3, 4]
    iex(9)> [element|list]
    [1, 2, 3, 4]
    
    # right side of "="
    iex(10)> list = [1|[1,2,3]]
    [1, 1, 2, 3]

    Back to our Agent…

    def login(user) do
        # get the current state
        s = get()
        # increment our user counter
        new_state = Map.put(s,:user_count,s.user_count + 1)
        IO.puts inspect new_state
        # add our user to our users list
        new_state = Map.put(new_state,:users,[user|new_state.users])
        IO.puts inspect new_state
        # store the update
        Agent.update(__MODULE__,fn state -> new_state end)
        # stub to broadcast a change event
        bcast
      end

    The rest of the agent is pretty straight forward if you understand the partial application syntax.

    Setup phoenix channels and sockets

    Step one here is to look at our endpoint, and ensure we have our sockets mapped correctly.

    lib/reflux_eventbroker_react_phoenix_elixir/endpoint.ex

    defmodule RefluxEventbrokerReactPhoenixElixir.Endpoint do
      use Phoenix.Endpoint, otp_app: :reflux_eventbroker_react_phoenix_elixir
    
      # commenting this out caused me all kinds of problems.  Seems to be some leftover assumptions this exists.
      socket "/socket", RefluxEventbrokerReactPhoenixElixir.UserSocket
      
      # this plumbs our socket path to our Socket functions in web/channels/pub_chat_socket.ex
      socket "/status",Reflux.PubChatSocket
      
    # SNIP

    web/channels/pub_chat_socket.ex

    Phoenix web sockets break things into sockets and channels. Sockets allow you to manage connections and authenticate a particular websocket path. They also allow you to manage the transport.

    defmodule Reflux.PubChatSocket do
      require Logger
      use Phoenix.Socket
      
      # Defines our channel name, and what Elixir module will be used to control it, PubChannel in this case
      
      channel "all",PubChannel 
      
      # Defines the transport, and if we need to check the host origin.  Check origin is useful if you want to limit access to your sockets to certain hosts
      
      transport :websocket, Phoenix.Transports.WebSocket, check_origin: false
    
      # connect parses our connection parameters from our client.  using phoenix.js this is socket.connect(params);
      # we also use Phoenix.Socket.assign/3 to embed our user and pass into the socket struct, which gets passed along to out channel.
      
      def connect(params, socket) do
        Logger.info "PARAMS: \n " <> inspect params
        socket = assign(socket, :user, params["user"])
        socket = assign(socket, :pass, params["pass"])
        {:ok,socket}
      end
    
      # id allows us to broadcast to all users with a particular id.  I'm not using this in this revision.
      
      def id(socket) do
        Logger.info("id called" <> inspect socket, pretty: true)
       "users_socket:#{socket.assigns.user}"
      end
    end

    So now we have our channel “all” mapped to our channel logic.

    web/channels/pub_channel.ex

    • join/3 : manages client join requests
    • handle_info/2 : manages our state update broadcasts
    • handle_in/3 : manages any messages sent to the channel after join has completed successfully
    • terminate/2 : manages when a websocket connection is no longer active

    Channels use the behaviour pattern. Behaviours allow us structure and composition. They are most heavily used in OTP patterns like GenServer. Behaviours generally lean heavily on pattern matching in function definition, which is worth of discussion for folks new to Elixir.

    Take the following definitions

    defmodule Foo do
        #1
        def bar(:atom) do
            "got an atom"
        end
        #2
        def bar({a,b}) do
            "got a 2 tuple with variables a and b assigned the arg's tuple values"
        end
        #3
        def bar(%{foo: foo} = arg) do
            "got a map with a key of :foo, interpolated into the variable 'foo', and the full map assigned to 'arg'"
        end
        #4
        def bar(%{"foo" => foo} = arg) do
            "foo key was a binary"
        end
        #5
        def bar(any) do
            any
        end
    end

    Elixir will take any call to Foo.bar(arg) and try to see if the argument fits a definition. This works top to bottom. The last case #5 will match any call to Foo.bar/1. Having a catch all can be useful in debugging to detect and crash when you have unexpected input. Example #1 will only match for Foo.bar(:atom). Example #2 will only match a 2 element tuple. Example #3 is much more interesting and powerful.

    Elixir map pattern matching allows you to look inside the argument and use different function definitions based on the keys of the map. In this case we will only match #3 if we use a map as an argument, and that map has a key of :foo. If we want access to the rest of the map we can use the arg variable. We can pass any map containing the key :foo %{foo: 1,bar: 2} will match, but %{“foo” => 1} will match #4 because the key is a binary (string). When you are serializing data to javascript it is best to use binaries as strings. Binaries also have very powerful pattern matching capabilities you may wan to explore.

    For phoenix channels we need join/3, and handle_in at a minimum.

    def join("all",payload,socket) do
        #  socket.assigns.user is assigned in our Socket
        user = socket.assigns.user
        
        # register the login event with our Agent
        LogAgent.login(user)
        Logger.info "User #{user} logged in: payload: #{inspect p}"
        
        # we can't broadcast from here so we call to handle_info
        send self,:status_update
        
        # return ok, and a "welcome" message to the client joining
        {:ok,"welcome",socket}
      end

    In this commit I have a defunct catchall def join, below I’ve fixed it to catch any joins with the wrong channel name. We could provide additional authentication checks in our first def join, and catch issues here.

    def join(any,s,socket) do
        Logger.error("unkown channel: #{inspect any} for assigns #{inspect socket.assigns}") 
        {:error, %{reason: "unauthorized"}}
      end

    Next is handle_info which broadcasts to all clients who have joined our “all” channel

    def handle_info(:status_update,socket) do
        Logger.info "handle_info :status_update"
        
        # broadcase!/3 sends an event "status_users" with the current state from our LogAgent
        # it wouldn't be a bad idea to throttle this for a large number of clients
        broadcast! socket, "status_users", LogAgent.get
        
        # we don't need a reply since we just used broadcast
        {:noreply, socket}
      end

    I have added a few events in a number of handle_in/3 definitions. :status_update, “status_users”,”ping”,”hit”, and any_event They all work pretty much the same, any_event is a catchall for errors. Hit does the most work for our use case. Notable here is the use of send. This is generically the way Elixir processes communicate between each other. In this case we use self() which returns the current PID, and matches to def handle_info(:status_update,socket). You can read more about send here

    def handle_in("hit",p,socket) do
        Logger.info "Hit from #{socket.assigns.user}"
        
        # update our state
        
        LogAgent.hit
        
        # call the broadcast for all connected users
        
        send self,:status_update
        {:noreply,socket}
      end

    Finally for our Channel we need to handle clients leaving. We define terminate/2 to update our state and user count

    def terminate(reason,socket) do
        # this test for assigns.user should never happen if our socket is doing it's job
        
        if socket.assigns.user != nil, do: LogAgent.logout(socket.assigns.user)
        
        Logger.info("terminated: #{inspect socket.assigns}")
        
        # I added this because I had some client terminations not notify, need to dig into why.  The messaging should 
        # be asynchronus, so there is a chance the state is not updated when we call :status_update
        :timer.sleep(50)
        
        # broadcast to all connected clients
        send self,:status_update
        :ok
      end

    reflux phoenix websocket client

    Now that we have our server all wired up to talk to clients, we can dig into the client code. Reflux will be managing all data from the server, and the react components will send their updates to the server which end up propagating back down to reflux to update our state.

    First we add a new action called “hit”

    web/static/js/Actions.js

    export default Reflux.createActions([
      "swap",
      "hit"
    ]);

    Next we update our reflux store to connect to phoenix

    web/static/js/stores/TheStore.js

    import Actions from "../Actions";
    
    export default Reflux.createStore({
      // binds our onSwap and onHit functions
      listenables: Actions,
    
      init() {
        this.test = true;
        
        // no logging
        //this.socket = new Phoenix.Socket("/status")
        
        // This creates our socket and sets up logging as an option
        this.socket = new Phoenix.Socket("/status",{logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }})
        
        // lazily create a semi unique username
        var r = Math.floor((Math.random() * 1000) + 1);
        this.me = "me"+r
    
        // these are our auth params which get sent to both connect/2 in our phoenix socket and join/3 in our phoenix channel
        this.auth = {user: this.me,pass: "the magic word"}
        
        // this maps our params to our socket object
        this.socket.connect(this.auth)
        
        // callbacks for varrious socket events
        this.socket.onOpen(this.onOpen)
        this.socket.onError(this.onError)
        this.socket.onClose(this.onClose)
        
        // configure our channel for "all"
        this.user_chan = this.socket.channel("all")
        console.log("chan", this.user_chan)
        
        // bind a function to any message with an event called "status_users"
        this.user_chan.on("status_users",data => {
          console.log("chan on hook",data);
          
          // blindy push data from server into our state
          this.onUpdate(data)
        })
        
        // this is what actually joins the "all" channel.  When the server responds "ok" and the join is successful we can 
        // drive other events, we just log it here.
        this.user_chan.join(this.auth).receive("ok", chan => {
          console.log("joined")
         })
         // callback for any errors caused by our join request
         .receive("error", chan => {
            console.log("error",chan);
        })
      },
      // pass our init() to React's state
      getInitialState(){
        return this;
      },
      onOpen(thing){
        console.log("onOpen",thing, this)
      },
      onClose(){
        console.log("onClose")
      },
      onError(){
        console.log("onError") 
      },
      onUpdate(update){
        console.log("update",update);
        console.log("this",this);
        
        // trigger is what will push our new state to React
        this.trigger(update)
      },
      // This is bound by our Actions.js.  it pushes a message to handle_in("hit","hit",socket) which increments a hit counter
      // this is triggered in our onClick handler for BtnA and BtnB
      onHit(){
        this.user_chan.push("hit","hit")
      },
      // our old swap action
      onSwap(x){
        console.log("switch triggered in: ",x)
        console.log("TheStore test is",this.test)
        this.trigger({test: !x})
      }
    })

    We add a new component to handle our user status data

    web/static/js/components/UserStatus.js

    import TheStore from "../stores/TheStore"
    
    export default React.createClass({
    
      // wire in our reflux store
      mixins: [Reflux.connect(TheStore)],
        
        // initial values in case the server is not connecting
        getInitialState(){
            return({user_count: 0, hits: 0, users: []} )
        },
        render: function() {
            var doItem = function(item){
              return (<span> name: {item} </span>)
            }
            return (
                <div className="panel panel-default">
                    <div className="panel-heading">
                        Status: me: {this.state.me} -- hits: <span clasName="badge">{this.state.hits}</span> 
                    </div>
                    <div className="panel-body">
                        Current Users: {this.state.users.map(doItem)} <span className="badge">{this.state.user_count}</span> 
                        Hits: <span className="badge">{this.state.hits}</span>
                    </div>
                </div>
            );
        }
    });

    Finally we can update our BtnA and BtnB components. They are very much the same, so I’ll only walk through one.

    import Actions from "../Actions"
    import TheStore from "../stores/TheStore"
    
    export default React.createClass({
        mixins: [Reflux.connect(TheStore)],
        getInitialState(){
            return {"name":"BtnA"};
        },
        handleClick(){
          console.log(this.state.name,"clicked",this.state.test);
          Actions.swap(this.state.test)
          
          // This triggers our onHit function in TheStore.js which pushes our event up to phoenix
          Actions.hit();
        },
        render(){
            return (
                <button className="btn btn-danger" onClick={this.handleClick}> 
                    This is {this.state.name}: val: {this.state.test.toString()} 
                </button>
            )
        }
    })

    That should be it! A working example can be found at https://dev.brng.us/rerpe

    %{description: "Using reflux.js react.js phoenix and elixir to connect components. This also describes the use of Elixir Agents, and Elixir's Behaviours.", title: "Wiring reflux and react to phoenix websockets"}



  • (Optional) See our JavaScript configuration variables documentation for ways to further customize the Disqus embed code.

Links

About this blog: This blog will discuss technology associated with my idgon.com project. I am using Elixir and Phoenix for my backend, and React.js and Reflux for my front end. I have a library called Trabant to experiment with graph database persistence for Elixir. The views expressed on this blog are my own, and are not that of my current employer.

About Me: I am a hobbyist programmer interested in distributed computing. I dabble in Elixir, Ruby, and Javascript. I can't spell very well, and I enjoy golf.