Calling DOM methods from ReasonReact reducer

reasonreact
interop

#1

Hi, I’m new to Reason and React.
I’m trying to write a component that needs to play an audio sound on ReasonReact action.
I have an <audio id="beep"> tag with autoPlay set to false in component render and I don’t know how to invoke its play method.
I did it before in JS:

var beep = document.getElementById("beep");
beep.play()

Is there a standard and safe way of calling DOM methods from reducer?


#2

Normally you’d use a React ref for this: https://reasonml.github.io/reason-react/docs/en/react-ref.html

The basic idea is that you store a reference to the DOM element inside some component state. Then when you want to play the audio you can access it from your internal state and call the play method on it.


#3

I was looking for something pure, without directly mutating state. The example with ref and := looks suspicious when you come from Elm :wink:


#4

I tried that, but don’t know how to cast given ref to audio.

switch (beepRef^) {
| Some(beep) => beep#play()                                                    
| None => Js.log("No beep sound to play")
};

results in error:

This expression has type Dom.element
It has no method play

#5

It’s actually quite safe provided you follow the documentation and model the mutation in a type-safe way:

  • The (Reason built-in) ref(something) type tells you that there is mutation going on
  • The option(something) type tells you that something may or may not be there, so you need to check for both cases

The safety comes from modelling even side-effectful code using the correct types, and restricting mutation to as small a scope as possible. These are fundamental techniques in FP. Admittedly, Elm promises a bit more safety by restricting what you can do, but there’s always a tradeoff.

Right, so normally I’d say to use the bs-webapi bindings, but looks like there’s none for audio … yet. I’d recommend writing a play binding yourself against an abstract audioElement type and just casting the ref’s value (beep) to audioElement to legally be able to call the binding.


#6

I have this from last year, never finished with the full example app: https://gist.github.com/sgrove/44a949092da8e025701abd74f7563a0f (here’s the app https://github.com/sgrove/reason-react-example/tree/kandan-2/src/kandan in old Reason syntax, big thanks to @rickyvetter for making it so easy to upgrade old syntax to new, otherwise this would have been a dead code base)

The DOM api may have changed a bit, there was a lot of churn at the time, but hopefully it should help a bit.


#7

I believe you’re supposed to do this as a side-effect.

reducer: (action, state) =>
    switch action {
    | PlayBeep =>
      ReasonReact.UpdateWithSideEffects(
        {...state, beeped: true},
        (self => playBeep(self))
      );
    },

#8

It would be very helpful to have a simple end-to-end example on how to play an audio element based on the recommendations above.


#9

I did it in a different way:

module JsAudio = {
  type audio;
  [@bs.new] external make : string => audio = "Audio";
  [@bs.send] external play : audio => unit = "play";
};

JsAudio.( make("sound.mp3") |> play )

#10

Great, that works fine.