Mastodon hachyterm.io

UPDATE:
ReasonML + BuckleScript is now Rescript.
As the ecosystem has changed around those tools, this blog post is not accurate anymore.


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,
};

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

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

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,
}

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,
};

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 }

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,
};

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

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

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

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

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>;

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