LiveView Hooks: Handling Channel connection race condition between mounted()/updated()

We have a Channel-based web app that we’re putting into a LiveView component, however we have run into a suspected race condition in our Hook between joining a channel and setting state from response in mounted(), which pops up when we try to use channel response state set in mounted() in updated(). Every so often, the state isn’t set fast enough in mounted() and is undefined when updated() runs, so we get error: undefined.

Our hook code:

  mounted() {
    this.channel = socket.channel('channel:' + channel_id)

    this.channel.join().receive("ok", response => {
      this.someState = response.some_state;
    })
  },
  updated(){ 
      /// This sometimes fails with error: this.someState is undefined
      doSomething(this.someState) 
  }

Wondering if there is a way to block updated() from running until after mounted/channel join, maybe with something async inside mounted()?

Would prefer to not have to join this channel outside of its component, as it is component-specific.

1 Like

The function in receive in mounted() is async → it will be called after the join and once “ok” is received … mounted is going to return, updated is going to get called and the “ok” message from the channel may not have been received yet. There is not much you can do there other than:

  • check this.someState in updated(), and not call doSomething until it is defined
  • note that doSomething should be called in that case
  • call doSomething in the join.receive callback if needed

It’s a pretty classic ‘resuource has async setup, but events may be generated before it is ready’ problem.

Many thanks for the suggestions :slight_smile: Tried 1 and 2 already but when the negative outcome of this race condition occurs, updated will have already run before this.someState is set, so a check in this case will prevent the error while leaving doSomething untriggered. 3 does not really work for the pattern of this live_component/hook (more on this in a moment).

Also tried doing an async/await with the channel join (on the join() and also on a setter called in the join.response) however running async/await anywhere in our hook (even on an arbitrary call elsewhere) results in a regeneratorRuntime error. I’ll have to learn more about Hooks under the hood before I go down that webpack/babel rabbithole.

In the earlier Channels + JS iteration of this web app, doSomething did occur in the join.receive callback, however in our current component + JS hook structure calling doSomething in the join.receive will go against the pattern we’re using that operates on a value we’re getting in the DOM from the LiveView socket assigns.

The pattern we implemented in the interest of brevity:

mounted(){
    this.channel = socket.channel('channel:' + channel_id)

    this.channel.join().receive("ok", response => {
      this.valueFromChannel = response.valueFromChannel;
    })
}

 updated(){ 
     if (valueFromAssigns === "foo") {
          doSomething(this.valueFromChannel)
     } else if (valueFromAssigns === "bar"){
          doSomethingElse(this.valueFromChannel)
     }
 } 

Looks like we have to further break up our live_components in this case, which is probably for the best in the long run.

I am likely overlooking another way to accomplish this within the hook, but it does feel like the LiveView JS hook lifecycle could benefit from an intuitive way to achieve asynchronicity. I realize that the entire point of using LiveView is to manage DOM/state from the server, not from the JS, but in our quick-and-dirty conversion it would be convenient if we could block updated() until something like an async/await function completes, similar to React/Vue lifecycle methods/plain JS with async/await.

1 Like

So, what I do with LiveView is keep all the pubsub on the server side and only use the LiveView websocket for LiveView communication. This keeps things nice and simple :slight_smile:

That said … could you just cache the events in update until this.valueFromChannel is set? e.g.:

   if (this.valueFromChannel == null) {
      pendingUpdateEvents.push(valueFromAssigns);
   } else {
     performUpdateAction(valueFromAssigns);
   }
}```

and then in in the “ok” response function do something like

pendingUpdateEvents.forEach(update => performUpdateAction(update));
pendingUpdateEvents = [];

Flavor to taste :slight_smile:

2 Likes

Thanks for your engagement, the problem really comes from trying to put an existing channels application into a LiveView child component quickly so the pattern is all off for LiveView

Your caching solution is super interesting, but I just went with refactoring the LiveView component and moved initial state hydration to the parent index.ex :stuck_out_tongue: