1

I'm trying to figure out how to get the state of a component to update based on an external event, and in this case the external event is a message coming down an Elixir Phoenix channel.

So basically, I have a simple h1 tag and it must always reflect the latest thing that comes into the channel. So there are two interlinked questions:

a) how do I get the channel into the component? So far I have done it by passing the channel as a prop.

b) How do I handle messages coming into the channel inside the component? My "this.state.chan.on" doesn't work, and seems clumsy.

import socket from "./socket"
import React from "react"
import ReactDOM from "react-dom"

socket.connect()

// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("topic:subtopic", {})

channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })

class HelloWorld extends React.Component {
  state = {
    chan: this.props.channel,
    mess: this.props.message
  }

  this.state.chan.on("new_message", payload => {
    this.setState(prevstate => {
      return {mess: ${payload.body}}
    });
  })


  componentDidMount = () => {
    console.log("did mount Hello World")
  }

  render = () => {
    return (<h1>{this.state.mess}</h1>)
  }
}


ReactDOM.render(
  <HelloWorld message={1} channel={channel}/>,
  document.getElementById("hello-world")
)

What is the accepted way of doing this? How do I get messages from a channel or socket or whatever, generated outside of react and outside of the user interface, to affect the state of components, and related, how do I get the outside event piped into the component in the first place? Is it correct to put the channel into the component? Because that also seems to limit the channel's output to affecting only that component, and not other independent ones that I might want it to affect.

EDIT: Here is the compilation error message. Yes I get that my JS may not be correct but I'm getting the syntax error right there on the first this.state.chan.on:

17:55:13 - error: Compiling of web/static/js/app.js failed. L40:6 Unexpected token 
     38 |   }
     39 | 
   > 40 |   this.state.chan.on(
        |       ^
     41 | 
     42 |   componentDidMount = () => {
     43 |     console.log("did mount Hello World")
Stack trace was suppressed. Run with `LOGGY_STACKS=1` to see the trace. 
18:07:20 - error: Compiling of web/static/js/app.js failed. L40:6 Unexpected token 
     38 |   }
     39 | 
   > 40 |   this.state.chan.on("new_message", payload => {
        |       ^
     41 |     this.setState(prevstate => {
     42 |       return {mess: ${payload.body}}
     43 |     });
Stack trace was suppressed. Run with `LOGGY_STACKS=1` to see the trace. 
18:07:22 - error: Compiling of web/static/js/app.js failed. L40:6 Unexpected token 
     38 |   }
     39 | 
   > 40 |   this.state.chan.on("new_message", payload => {
        |       ^
     41 |     this.setState(prevstate => {
     42 |       return {mess: ${payload.body}}
     43 |     });
Stack trace was suppressed. Run with `LOGGY_STACKS=1` to see the trace. 
Thomas Browne
  • 23,824
  • 32
  • 78
  • 121
  • `My "this.state.chan.on" doesn't work` What part doesn't work? Does it throw an error? Does it not get called at all? – Dogbert Aug 22 '17 at 17:36
  • `this.setState(prevstate => { return {mess: ${payload.body}} });` this looks like a syntax error. – Dogbert Aug 22 '17 at 17:36
  • Edited to reflect the two above comments. Apols for my poor JS yes I may have a syntax error there, but there seems to be a problem even before that.... – Thomas Browne Aug 22 '17 at 17:41

1 Answers1

2

You cannot have this.state.chan.on(...) in the class body outside the functions. You can put all this code in the constructor though. Also, your setState call contains a syntax error and can be simplified to use an object as the argument. Here's how the constructor would look like:

class HelloWorld extends React.Component {
  constructor(props) {
    super();

    this.state = {
      chan: props.channel,
      mess: props.message
    };

    this.state.chan.on("new_message", payload => {
      this.setState({mess: payload.body});
    });
  }

  ...
}

There is one problem with this though. The on callback will keep getting fired even when this component is unmounted from the DOM. You should subscribe to messages in componentDidMount and unsubscribe in componentWillUnmount:

class HelloWorld extends React.Component {
  constructor(props) {
    super();

    this.state = {
      chan: props.channel,
      mess: props.message
    };
  }

  componentDidMount() {
    const ref = this.state.chan.on("new_message", payload => {
      this.setState({mess: payload.body});
    });
    this.setState({ref: ref});
  }

  componentWillUnmount() {
    this.state.chan.off("new_message", this.state.ref);
  }

  ...
}
Dogbert
  • 212,659
  • 41
  • 396
  • 397
  • 'setState(oldstate => newState)' is valid actually – webdeb Aug 22 '17 at 18:10
  • @webdeb yep, that part is valid, but `return {mess: ${payload.body}}` is a syntax error. – Dogbert Aug 22 '17 at 18:14
  • interestingly, and I don't know if it's me, but I'm getting an "undefined" in the this.state.ref. I did modify your code a bit: componentDidMount() { console.log(this.state); const ref = this.state.chan.on("counter", payload => { console.log(this.state); let thebody = payload.counter; this.setState({mess: thebody}); this.state.chan.push("counter", {body: thebody}) }); this.setState({ref: ref}); this.state.chan.push("counter", {body: "1"}) } – Thomas Browne Aug 22 '17 at 21:04
  • So I send the counter back down the channel and the channel bounces it back to me incremented by one. The updates are working fine - ie react is updating the dom. I'm just wondering why in my version the this.state.ref is undefined. – Thomas Browne Aug 22 '17 at 21:05
  • At what point is `this.state.ref` undefined? In `componentWillUnmount`? – Dogbert Aug 23 '17 at 05:28
  • Within the component I do a console.log(this.state) and that's where it's undefined. Does that make sense? perhaps it's not undefined once we get to the componentWillUnmount? Apols I'm a scientific programmer used to imperative style and js control flow and scope rules are still quite new to me. – Thomas Browne Aug 23 '17 at 10:28