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
- How to Use the useContext Hook in React by James King
- ReasonReact
- ReasonReact hook recipes by Paul Shen
- ReasonReact context explained in action by Margarita Krutikova
- GitHub Repository for the music player
Top comments (2)
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
You can do:
This also takes advantage of Reason's JSX props punning for further succinctness.
Oh, that's great. Thanks for pointing out this "trick"!