DEV Community

loading...

How To Create a Music Player in Reason With The useContext Hook Part 2

sophiabrandt profile image Sophia Brandt Originally published at rockyourcode.com on ・5 min read

In the last post, we set up our project: a music player with useContext in ReasonReact.

You can find the demo on GitHub pages and the full code on GitHub.

The tutorial is a port from the React tutorial How to Use the useContext Hook in React by James King.

Type-Driven Development

ReasonReact is a statically typed language. We should now think about our data model and create types. That will help to flesh out our app's state.

We need a model for a musicTrack. We need to convert each musicTrack into an HTML AudioElement. A music track is an mp3 file that we'll upload and bundle via webpack.

src/SharedTypes.re:

type musicTrack = {
  name: string,
  file: string,
};
Enter fullscreen mode Exit fullscreen mode

The above code shows a record type:

Records are like JavaScript objects but are

  • lighter
  • immutable by default
  • fixed in field names and types
  • very fast
  • a bit more rigidly typed

But we'll need more than one musicTrack, so let's create a type for a collection of tracks:

type musicTracks = array(musicTrack);
Enter fullscreen mode Exit fullscreen mode

Now, let's think about the app state. We have a collection of tracks that we'll want to play or pause. So the state needs to communicate if a track plays, which one it is, or if no track is playing:

type playing =
  | Playing(int) // track is playing and also has an index of type integer
  | NotPlaying;  // no track is playing
Enter fullscreen mode Exit fullscreen mode

Here we can see the power of ReasonML's type system. With JavaScript, you will have to keep track of isPlaying and the track's index. For example:

const initialState = {
  tracks: [
    { name: 'Benjamin Tissot - Summer', file: summer },
    { name: 'Benjamin Tissot - Ukulele', file: ukulele },
    { name: 'Benjamin Tissot - Creative Minds', file: creativeminds },
  ],
  isPlaying: false,
  currentTrackIndex: null,
}
Enter fullscreen mode Exit fullscreen mode

But that code could create a bug. Potentially we could both set isPlaying to true, but still have a currentTrackIndex of null. There should be a relationship between those two pieces, but we can't model that with React.js.

Of course, you could use libraries (i.e., xstate).

But ReasonML offers this functionality out of the box with variants.

(A variant is similar to a TypeScript enum.)

In our case, we can now finish our data model:

/* src/SharedTypes.re */

type musicTrack = {
  name: string,
  file: string,
};

type musicTracks = array(musicTrack);

type playing =
  | Playing(int)
  | NotPlaying;

type state = {
  tracks: musicTracks,
  playing,
};

Enter fullscreen mode Exit fullscreen mode

Create a Context

Here is the useMusicPlayerContext.js file from the original blog post:

import React, { useState } from 'react'

const MusicPlayerContext = React.createContext([{}, () => {}]) // creates Context

const MusicPlayerProvider = props => {
  const [state, setState] = useState({
    tracks: [
      {
        name: 'Lost Chameleon - Genesis',
      },
      {
        name: 'The Hipsta - Shaken Soda',
      },
      {
        name: 'Tobu - Such Fun',
      },
    ],
    currentTrackIndex: null,
    isPlaying: false,
  })
  return (
    // add state to Context Provider
    <MusicPlayerContext.Provider value={[state, setState]}>
      {props.children}
    </MusicPlayerContext.Provider>
  )
}

export { MusicPlayerContext, MusicPlayerProvider }
Enter fullscreen mode Exit fullscreen mode

As you can see, we can create a Context with an empty JavaScript object. Inside the Provider, we switch it out with a useState hook.

How can we do the same with ReasonReact?

Let's create the initial state for the app first. We already defined the type in src/SharedTypes.re:

/* src/MusicPlayer.re */

let initialState: SharedTypes.state = {
  tracks: [|
    { name: 'Benjamin Tissot - Summer', file: "summer" },
    { name: 'Benjamin Tissot - Ukulele', file: "ukulele" },
    { name: 'Benjamin Tissot - Creative Minds', file: "creativeminds" },
  |],
  isPlaying: false,
};

Enter fullscreen mode Exit fullscreen mode

It almost looks the same. Arrays use a different syntax than JavaScript ([||]), and we have to tell Reason that the initialState binding is of the type SharedTypes.state (which refers to the other file we already created).

let bindings are immutable, in case you're wondering.

We'll manage state with useReducer instead of useState. It works better with a record.

Let's create some dummy values:

type action =
  | DoSomething;

let reducer = (state: SharedTypes.state, action) =>
  switch (action) {
  | DoSomething => state
  };

Enter fullscreen mode Exit fullscreen mode

Now we can create the Context:

// the type of the dispatch function is action => unit
// initialize the Context with state and `ignore`

let musicPlayerContext = React.createContext((initialState, ignore));
Enter fullscreen mode Exit fullscreen mode

Now create the Provider and the main component. We'll use the MusicPlayer component in other modules of our app.

module MusicPlayerProvider = {
  let makeProps = (~value, ~children, ()) => {
    "value": value,
    "children": children,
  };
  let make = React.Context.provider(musicPlayerContext);
};

[@react.component]
let make = (~children) => {
  let (state, dispatch) = React.useReducer(reducer, initialState);

  <MusicPlayerProvider value=(state, dispatch)>
    children
  </MusicPlayerProvider>;
};
Enter fullscreen mode Exit fullscreen mode

Reason's way is more complex. I had to search for how useContext works in ReasonReact and fumble my way through.

Margarita Krutikova wrote an excellent blog post about ReasonReact's context, if you're interested.

Here is the Context file in its full glory:
src/MusicPlayer.re

let initialState: SharedTypes.state = {
  tracks: [|
    { name: 'Benjamin Tissot - Summer', file: "summer" },
    { name: 'Benjamin Tissot - Ukulele', file: "ukulele" },
    { name: 'Benjamin Tissot - Creative Minds', file: "creativeminds" },
  |],
  isPlaying: false,
};

type action =
  | DoSomething;

let reducer = (state: SharedTypes.state, action) =>
  switch (action) {
  | DoSomething => state
  };

let musicPlayerContext = React.createContext((initialState, ignore));

module MusicPlayerProvider = {
  let makeProps = (~value, ~children, ()) => {
    "value": value,
    "children": children,
  };
  let make = React.Context.provider(musicPlayerContext);
};

[@react.component]
let make = (~children) => {
  let (state, dispatch) = React.useReducer(reducer, initialState);

  <MusicPlayerProvider value=(state, dispatch)>
    children
  </MusicPlayerProvider>;
};

Enter fullscreen mode Exit fullscreen mode

We will be able to manage the app's state in this module. We'll use the MusicProvider to pass the state and the reducer function to other components of the app.

Add Context to Main App

It's easy to connect the context to the rest of the app. Go to src/App.re and include the MusicPlayer module:

open ReactUtils;

[@react.component]
let make = () =>
  <div className="section is-fullheignt">
    <div className="container">
      <div className="column is-6 is-offset-4">
        <h1 className="is-size-2 has-text-centered">
          {s("Reason Music Player")}
        </h1>
        <br />
        <MusicPlayer /> // * new *
      </div>
    </div>
  </div>;
Enter fullscreen mode Exit fullscreen mode

MusicPlayer will wrap two other components (TrackList and PlayerControls) which we'll create later. Those components will have access to the context.

Recap

In this post, we created the context for the music player application. We used types, useContext, and useReducer.

The syntax for ReasonReact is more complicated, but our types will minimize some bugs.

Further Reading

Discussion (2)

pic
Editor guide
Collapse
yawaramin profile image
Yawar Amin

Great post, love seeing type-driven development in action! :-)

One cute 'trick' that you can do, is keep a tuple 'as-is' if you're passing it forward to something else. So e.g. instead of

let (state, dispatch) = React.useReducer(reducer, initialState);
<MusicPlayerProvider value=(state, dispatch)>

You can do:

let value = React.useReducer(reducer, initialState);
<MusicPlayerProvider value>

This also takes advantage of Reason's JSX props punning for further succinctness.

Collapse
sophiabrandt profile image
Sophia Brandt Author

Oh, that's great. Thanks for pointing out this "trick"!