UPDATE:
ReasonML + BuckleScript is now Rescript.
As the ecosystem has changed around those tools, this blog post is not accurate anymore.
Our goal is to create a music player with ReasonReact like this one: ☞ Demo.
James King wrote the JavaScript version of the tutorial at upmostly. My blog post series is a port of the tutorial to ReasonML and ReasonReact.
I encourage you to check out the original JavaScript tutorial if you're unsure about how useContext
works.
ReasonReact compiles (via BuckleScript) to React. We can leverage our existing React knowledge. We have all the power of React, but with the type-safety of OCaml, a mature language.
In part 1 of the series, we set up the project.
In part 2 of the series, we created the MusicPlayer
component with a React Context.
Find the code repository on GitHub.
Manage State And Create a Custom Hook
We need a way to manage our state. The MusicPlayer
component has a useReducer
function that we can use.
We want to have the ability to play a track and to pause a track. If we play a track, we have to tell the program which one. We'll use the Array's index for that, as our tracks are in an Array.
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"},
|],
playing: NotPlaying,
};
type action =
| PauseTrack // (A)
| PlayTrack(int);
// when we pause a track, we need to transition to
// the `NotPlaying` state
//
let withPauseTrack = state: SharedTypes.state => {
...state,
playing: NotPlaying, // (B)
};
// when we play a track, we need to transition to
// the `PlayingState` and add the payload of the
// track's index
//
let withPlayTrack = (state: SharedTypes.state, index) => {
...state,
playing: Playing(index), // (B)
};
let reducer = (state: SharedTypes.state, action) =>
switch (action) { // (A)
| PauseTrack => withPauseTrack(state)
| PlayTrack(index) => withPlayTrack(state, index)
};
Several interesting things are going on here. Our action
is another variant; thus, we can pattern-match on it in the reducer function (A
).
Pattern-matching is one of the power features of ReasonML:
It's like destructuring, but comes with even more help from the type system.
What happens when you delete the NotPlaying
line in the reducer (A
)? Try it out! The compiler will give you a warning:
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
NotPlaying
Pattern-matching conveniently resembles JavaScript's switch syntax. But thanks to the compiler warnings, you can make sure that you handle all cases.
Furthermore, we update our immutable records by creating new records with the spread operator (B
).
That also looks like JavaScript!
Now that we know how to handle state, we'll create a custom hook that will manage Context.
Let's see how it would look like in JavaScript:
import { useContext } from 'react'
import { MusicPlayerContext } from '../MusicPlayerContext'
const useMusicPlayer = () => {
const [state, setState] = useContext(MusicPlayerContext)
function playTrack(index) {
if (index === state.currentTrackIndex) {
togglePlay()
} else {
setState(state => ({
...state,
currentTrackIndex: index,
isPlaying: true,
}))
}
}
function togglePlay() {
setState(state => ({ ...state, isPlaying: !state.isPlaying }))
}
function playPreviousTrack() {
const newIndex =
(((state.currentTrackIndex + -1) % state.tracks.length) +
state.tracks.length) %
state.tracks.length
playTrack(newIndex)
}
function playNextTrack() {
const newIndex = (state.currentTrackIndex + 1) % state.tracks.length
playTrack(newIndex)
}
return {
playTrack,
togglePlay,
currentTrackName:
state.currentTrackIndex !== null &&
state.tracks[state.currentTrackIndex].name,
trackList: state.tracks,
isPlaying: state.isPlaying,
playPreviousTrack,
playNextTrack,
}
}
export default useMusicPlayer
And now in Reason:
src/useMusicPlayer.re
// a hook is a function
let useMusicPlayer = () => {
// here we'll load our Context
// it's the same as in JavaScript
//
let (state, dispatch) = React.useContext(MusicPlayer.musicPlayerContext);
let playing = state.playing;
let trackList = state.tracks;
// find the current track name
// we can pattern-match on our state
// if we are in the state of `Playing`, then find the name of the
// index of the tracks Array
// if we don't play anything, we can't have a name, so we'll use
// a placeholder string
// ReasonML can infer types, so we don't have to tell the program
// that the `currentTrackName` is a string
//
let currentTrackName =
switch (playing) {
| Playing(idx) => state.tracks[idx].name
| NotPlaying => "Please choose a track to play"
};
// this function dispatches to `MusicPlayer` with the
// `PauseTrack` action we defined earlier
//
let pauseTrack = () => MusicPlayer.PauseTrack |> dispatch;
// here we dispatch to the `PlayTrack(index)` action we defined
// in `src/MusicPlayer.re`
//
let playTrack = index =>
switch (playing) {
| Playing(idx) =>
index === idx ?
pauseTrack() :
{
// here we use the pipe operator
// this is the same as
// dispatch(MusicPlayer.PlayTrack(index))
MusicPlayer.PlayTrack(index) |> dispatch; // (A)
}
| NotPlaying => MusicPlayer.PlayTrack(index) |> dispatch
};
let trackListLength = Array.length(trackList);
let playPreviousTrack = _ =>
switch (playing) {
| Playing(idx) =>
((idx - 1) mod trackListLength + trackListLength)
mod trackListLength
|> playTrack
| NotPlaying => ()
};
let playNextTrack = _ =>
switch (playing) {
| Playing(idx) => (idx + 1) mod trackListLength |> playTrack
| NotPlaying => ()
};
(
playing,
trackList,
currentTrackName,
pauseTrack,
playTrack,
playPreviousTrack,
playNextTrack,
);
};
I tend to use the pipe operator (|>
) to chain functions (see line A
).
The current ReasonML documentation is a bit sparse:
let
(|>)
:('a, 'a => 'b) => 'b;
Reverse-application operator:x |> f |> g
is exactly equivalent tog (f (x))
.
Creating the UI
We now have a MusicPlayer
component that contains the React context (including state and a dispatch function to handle state transitions) and a custom useMusicPlayer
hook.
Let's update src/App.re
:
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> <TrackList /> </MusicPlayer>// * new *
</div>
</div>
</div>;
Create src/TrackList.re
:
open ReactUtils;
[@react.component] // (A)
let make = () => {
let (
playing,
trackList,
_currentTrackName,
_pauseTrack,
playTrack,
_playPreviousTrack,
_playNextTrack,
) =
UseMusicPlayer.useMusicPlayer(); // (B)
<>
{
Array.mapi( // (C)
(index, track: SharedTypes.musicTrack) => // (D)
<div className="box" key={index |> string_of_int}> // (E)
<div className="columns is-vcentered">
<button className="button"
onClick={_ => playTrack(index)}> // (F)
{
switch (playing) { // (G)
| Playing(idx) =>
idx === index ?
<i className="fas fa-pause" /> :
<i className="fas fa-play" />
| NotPlaying => <i className="fas fa-play" />
}
}
</button>
<div className="song-title column">
{s(track.name)}
</div>
</div>
</div>,
trackList,
)
|> React.array // (H)
}
</>;
};
First, we create a new React component (A
). Then we use our custom hook to gain access to the state and the functions that control the app's state.
We don't need everything we've exported from useMusicPlayer
. Thus we can append an underscore under the variables we don't use (B
).
Similar to React.js, we map over the collection (Array) of our tracks. We use ReasonML's Array.mapi
to map with an index (C
). Array.mapi
takes a function first and the collection as the second parameter.
Unfortunately, that means that we have to tell Reason the type of musicTrack
(see line D
).
We also have to convert the Array's index (an integer) to a string (D
). Luckily, if you forget that, the compiler will help you out and throw an error.
When we click the button, we want to fire off the playTrack
function from the custom useMusicPlayer
hook (F
).
On line G
, we decide what kind of button to show. If we don't play any track, then show a "play" button. But if we play a track, we only want to show the "play" button for those tracks that aren't currently playing. For the playing track, we want to show a "pause" button.
We use Bulma and FontAwesome icons for the icons.
Lastly, we have to convert the Reason Array into a React.array (H
).
Recap
We created a custom hook and also added state management to our application.
We started fleshing out the UI with the TrackList
component.
Coming Next
In the next posts, we'll create a component for controlling the music player. We'll also create an HTML Audio Element that will allow us to play the mp3 file.
Top comments (1)
Very cool, great to see this progressing.
playTrack
can be a little more concise:This captures the fact that we pause only when the current index is playing, and play otherwise; and that we need to dispatch from both branches.