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.
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);
}
...
}