CDGD

// TODO fix this bullshit later

A real Reason component

October 01, 2018

In my last post I added support for ReasonML and made a very simple ReasonML component. This time around I dug into it some more and made a component that actually does something. Let’s play with reducerComponent, state, reducers, actions & event handling!

let component = ReasonReact.statelessComponent("RandomSaying");

let make = children => {
    ...component,
    render: _self => <p> children </p>
};

let default = ReasonReact.wrapReasonForJs(~component, jsProps => make(jsProps##children));

Double quotes, eh? Let’s change that to single quotes:

let component = ReasonReact.statelessComponent(RandomSaying);
ℹ 「wdm」: Compiling...
 ERROR  Failed to compile with 1 errors                                                                                 6:34:47 AM

 error  in ./src/components/RandomSaying.re

Module build failed (from /usr/local/lib/node_modules/bs-loader/index.js):
Error: File "/Users/gabe/Desktop/gatsby-v2/cdgd/src/components/RandomSaying.re", line 1, characters 47-48:
Error: 1257: <syntax error>

Oh shit. Maybe not. Double quotes it is.

Ok, so let’s throw some text in this paragraph directly instead of passing in children.

let make = children => {
    ...component,
    render: _self => <p> reasonable </p>
};
✖ 「wdm」: 
ERROR in ./src/components/RandomSaying.re
Module build failed (from /usr/local/lib/node_modules/bs-loader/index.js):
Error: ninja: no work to do.
[2/2] Building src/components/RandomSaying.mlast.d
[1/1] Building src/components/RandomSaying.cmj

  We've found a bug for you!
  /Users/gabe/Desktop/gatsby-v2/cdgd/src/components/RandomSaying.re 5:26-35
  
  3 │ let make = children => {
  4 │     ...component,
  5 │     render: _self => <p> reasonable </p>
  6 │ };
  7 │ 
  
  The value reasonable can't be found

Oh, wait, right. Gotta do that nasty ReasonReact.string shit. And I remember from Ken’s talk that you have to prefix arguments with an underscore if they’re not used, and we’re not using children, but I also don’t want to fuck with function signatures right now, just in case.

let make = _children => {
    ...component,
    render: _self => <p> (ReasonReact.string(“is this reasonable?”)) </p>
};
ℹ 「wdm」: Compiling...
 DONE  Compiled successfully in 83ms    

Cool!

Now, let’s change this to a heading tag and style it a little bit to remove some of the whitespace.

Uh. Wait, how the fuck do style props work in ReasonReact?

googles

Here we go

<div style=(
  ReactDOMRe.Style.make(~color="#444444", ~fontSize="68px", ())
)/>

Uhh… Fucking gross. Whatever.

let make = _children => {
    ...component,
    render: _self => 
        <h4 style=(ReactDOMRe.Style.make(~marginTop="0px", ()))>
            (ReasonReact.string("is this reasonable?"))
        </h4>
};

Man… Note to self: see if there’s styled components or any kind of CSS-in-Reason shit later. But, I’m guessing no. I don’t recall there being tagged template literals. Barf.

Ok, so I’m going to just create a static list of brief sayings to use as sub-headers for the CDGD site. Again, from Ken’s talk, I remember the format to declare an array is brackets and pipes, like this:

let sayings: array(string) = [|
    "Chill Dev, Grumpy Dev", 
    "const chillOrGrumpy = Math.random() > 0.5", 
    "grumble grumble grumble"
|];

There’s a difference between arrays and lists in ReasonML. I forget what it is. Hopefully this is the right one to use.

So, now I just need to get a random entry from that array.

googles

What the fuck? How is there not a simple example of this anywhere? It’s like one of the most basic things! I have to question a language that doesn’t have a bunch of good examples out there already. This page comes up first, and doesn’t answer my question at all. Thanks, google!

You’re up, DDG.

DuckDuckGo results about Java  What the SHIT?

Back to google we go!

Ok, here’s the ReasonML docs page for lists and arrays. How do I get the length of an array? Oh. No mention of “length” on this page? Ooooookayyyyyyy…. Well, there’s a separate API page. Maybe it’s there? Ok, Array API. Got it. And there it is. Array.length.

Ok, so just to try this out, I’m going to use Js.log(), which calls console.log() and see if I can get the array length.

let make = children => {
    ...component,
    render: _self => {
        let sayings: array(string) = [|
            "Chill Dev, Grumpy Dev", 
            "const chillOrGrumpy = Math.random() > 0.5", 
            "grumble grumble grumble"
        |];
        Js.log(sayings.length);
        <p> (ReasonReact.string("is this reasonable?")) </p>
    }
};
  We've found a bug for you!
  /Users/gabe/Desktop/gatsby-v2/cdgd/src/components/RandomSaying.re 17:24-29
  
  15 │     "grumble grumble grumble"
  16 │ |];
  17 │         Js.log(sayings.length);
  18 │         <p> (ReasonReact.string("is this reasonable?")) </p>
  19 │     }
  
  The record field length can't be found.

Oh for fuck’s sake. Do I really have to pass the array to Array.length()? Fuck, right. It’s not JavaScript. Not everything is an object, so there’s no methods/properties on these things.

let make = children => {
    ...component,
    render: _self => {
        let sayings: array(string) = [|
            "Chill Dev, Grumpy Dev", 
            "const chillOrGrumpy = Math.random() > 0.5", 
            "grumble grumble grumble"
        |];
        Js.log(Array.length(sayings));
        <p> (ReasonReact.string("is this reasonable?")) </p>
    }
};
ℹ 「wdm」: Compiling...
 DONE  Compiled successfully in 91ms                                                                                    6:56:53 AM

And it logged “3” in the dev console. Ok, that works for me.

Now how the fuck do I get a random number? I guess I’ll just look at that API page again since the other docs suck. Well, there’s no “Math” section. But there is a Random API. Ok, so we do Random.int(max). Got it. So, our random array access should look something like this, I think:

sayings[Random.int(Array.length(sayings))]

Oh, wait. Looking at that API again, I think we should use Random.int32() since Random.int() has a max of 230 for some reason. Not that I’m going to have 231+ sayings in here, but, it’d probably help avoid issues long term if I get used to int32 instead.

  We've found a bug for you!
  /Users/gabe/Desktop/gatsby-v2/cdgd/src/components/RandomSaying.re 17:37-57
  
  15 ┆         "grumble grumble grumble"
  16 ┆     |];
  17 ┆     Js.log(sayings[Random.int32(Array.length(sayings))]);
  18 ┆     <p> (ReasonReact.string("is this reasonable?")) </p>
  19 ┆ }
  
  This has type:
    int
  But somewhere wanted:
    Int32.t (defined as int32)

Fucking WHAT? Ok, Random.int it is.

So I should just be able to do this, then. Right?

let make = _children => {
    ...component,
    render: _self => 
    <h4 style=(ReactDOMRe.Style.make(~marginTop="0px", ()))>
        (ReasonReact.string(sayings[Random.int(Array.length(sayings))]))
    </h4>
};

Oh shit. That worked! But all my text is uppercase and left aligned. Bleh.

add a little styling

let make = _children => {
    ...component,
    render: _self => 
    <h4 
        style=(ReactDOMRe.Style.make(~marginTop="0px", ~textAlign="center", ~textTransform="none", ()))
    >
        (ReasonReact.string(sayings[Random.int(Array.length(sayings)) - 1]))
    </h4>
};

Nice. Ok. Next, let’s add a click event on this sub-header and change the text to a random value when it’s clicked.

let make = _children => {
    ...component,
    render: _self => 
    <h4 
        style=(ReactDOMRe.Style.make(~marginTop="0px", ~textAlign="center", ~textTransform="none", ()))
        onClick=((evt) => Js.log("didn't add something"))
    >
        (ReasonReact.string(sayings[Random.int(Array.length(sayings))]))
    </h4>
};

Cool. That works. Now, how do I get the click handler to change it? In JS we just add a class method and call that to update state, so maybe I can do something like this?

let make = _children => {
    ...component,
    getSaying: () => sayings[Random.int(Array.length(sayings)) - 1],
    render: self => 
    <h4 
        style=(ReactDOMRe.Style.make(~marginTop="0px", ~textAlign="center", ~textTransform="none", ()))
        onClick=((evt) => Js.log("ayo"))
    >
        (ReasonReact.string(self.getSaying())
    </h4>
};

Uh. Nope.

ℹ 「wdm」: Compiling...
 ERROR  Failed to compile with 2 errors                                                                                 4:12:41 PM

 error  in ./src/components/RandomSaying.re

Module Error (from /usr/local/lib/node_modules/bs-loader/index.js):
File "/Users/gabe/Desktop/gatsby-v2/cdgd/src/components/RandomSaying.re", line 19, characters 4-9:

Do I have to declare these handlers externally and pass them into the component creation function?

let onClick = (evt) => Js.log("clicked");

let getSaying = () => sayings[Random.int(Array.length(sayings)) - 1];

let make = (~onClick, ~getSaying, _children) => {
  let click = (event, self) => {
    onClick(event);
  };
  {
    ...component,
    initialState: {},
    render: self => 
    <h4 
        style=(ReactDOMRe.Style.make(~marginTop="0px", ~textAlign="center", ~textTransform="none", ()))
        onClick=((evt) => Js.log("ayo"))
    >
        (ReasonReact.string(getSaying()))
    </h4>
  }
};


let default = ReasonReact.wrapReasonForJs(~component, jsProps => make(onClick, getSaying, jsProps##children));

Ugh.

ℹ 「wdm」: Compiling...
 ERROR  Failed to compile with 1 errors                                                                                 4:15:34 PM

 error  in ./src/components/RandomSaying.re

Module build failed (from /usr/local/lib/node_modules/bs-loader/index.js):
Error: File "/Users/gabe/Desktop/gatsby-v2/cdgd/src/components/RandomSaying.re", line 19, characters 4-9:
Error: 1254: <syntax error>

I’m just going down the wrong rabbit hole here. You can’t add your own stuff to the component record because it’s type-checked and looks for specific entries only. But you can declare things above that in the function body of make.

google-fu

Okay. So it looks like I need to use a reducerComponent and use state and actions to handle this.

I’m going to create an action called ChangeSaying, and create a type for state that’s just the saying item. Then I add in a reducer for the action I created, and I can have that call getSaying() and update state.

let sayings: array(string) = [|
    "Chill Dev / Grumpy Dev", 
    "const isDevGrumpy = Math.random() > 0.5;", 
    "grumble grumble grumble",
    "Whiskey Tango Foxtrot",
    "// TODO fix this bullshit later"
|];

let component = ReasonReact.reducerComponent("RandomSaying");

type state = {saying: string};
type action =
  | ChangeSaying;

let getSaying = () => sayings[Random.int(Array.length(sayings))];

let make = (_children) => {
    {
        ...component,
        initialState: () => {saying: getSaying()},
        reducer: (action, state) =>
            switch (action) {
            | ChangeSaying => ReasonReact.Update({...state, saying: getSaying()})
            },
        render: self => 
            <h4 
                style=(ReactDOMRe.Style.make(~marginTop="0px", ~textAlign="center", ~textTransform="none", ()))
                onClick=(_event => self.send(ChangeSaying))
            >
                (ReasonReact.string(self.state.saying))
            </h4>
    }
};

let default = ReasonReact.wrapReasonForJs(~component, jsProps => make(jsProps##children));

Ohhhhhh boy.

ℹ 「wdm」: Compiling...
 ERROR  Failed to compile with 1 errors                                                                                 4:27:42 PM

 error  in ./src/components/RandomSaying.re

Module build failed (from /usr/local/lib/node_modules/bs-loader/index.js):
Error: ninja: no work to do.
[2/2] Building src/components/RandomSaying.mlast.d
[1/1] Building src/components/RandomSaying.cmj

  We've found a bug for you!
  /Users/gabe/Desktop/gatsby-v2/cdgd/src/components/RandomSaying.re 22:12-20
  
  20 │ let make = (_children) => {
  21 │     {
  22 │         ...component,
  23 │         
  24 │         initialState: () => {saying: getSaying()},
  
  Is this a ReasonReact reducerComponent or component with retained props?
  If so, is the type for state, retained props or action declared _after_
  the component declaration?

Oh, fuck. Right. Types have to go above the component. Ken mentioned that in his talk, I think. D’oh!

let sayings: array(string) = [|
    "Chill Dev / Grumpy Dev", 
    "const isDevGrumpy = Math.random() > 0.5;", 
    "grumble grumble grumble",
    "Whiskey Tango Foxtrot",
    "// TODO fix this bullshit later"
|];

type state = {saying: string};

type action =
  | ChangeSaying;

let component = ReasonReact.reducerComponent("RandomSaying");

let getSaying = () => sayings[Random.int(Array.length(sayings))];

let make = (_children) => {
    {
        ...component,
        initialState: () => {saying: getSaying()},
        reducer: (action, state) =>
            switch (action) {
                | ChangeSaying => ReasonReact.Update({saying: getSaying()})
            },
        render: self => 
            <h4 
                style=(ReactDOMRe.Style.make(~marginTop="0px", ~textAlign="center", ~textTransform="none", ()))
                onClick=(_event => self.send(ChangeSaying))
            >
                (ReasonReact.string(self.state.saying))
            </h4>
    }
};

let default = ReasonReact.wrapReasonForJs(~component, jsProps => make(jsProps##children));
ℹ 「wdm」: Compiling...
 DONE  Compiled successfully in 162ms   

Yes! It works.

But hey, I can probably take out the children parts, because I’m not passing any in.

let sayings: array(string) = [|
    "Chill Dev / Grumpy Dev", 
    "const isDevGrumpy = Math.random() > 0.5;", 
    "grumble grumble grumble",
    "Whiskey Tango Foxtrot",
    "// TODO fix this bullshit later"
|];

type state = {saying: string};

type action =
  | ChangeSaying;

let component = ReasonReact.reducerComponent("RandomSaying");

let getSaying = () => sayings[Random.int(Array.length(sayings))];

let make = () => {
    {
        ...component,
        initialState: () => {saying: getSaying()},
        reducer: (action, state) =>
            switch (action) {
                | ChangeSaying => ReasonReact.Update({saying: getSaying()})
            },
        render: self => 
            <h4 
                style=(ReactDOMRe.Style.make(~marginTop="0px", ~textAlign="center", ~textTransform="none", ()))
                onClick=(_event => self.send(ChangeSaying))
            >
                (ReasonReact.string(self.state.saying))
            </h4>
    }
};

let default = ReasonReact.wrapReasonForJs(~component, _jsProps => make());

Well, I don’t know if I’d call it perfect, but that works. I am definitely still thinking in JavaScript and that’s my biggest problem with ReasonML so far. I’m going to play around with this some more and try and get used to it.


Gabriel Ricard

Stream of consciousness software fuckery.
Assembled by Gabriel Ricard, software developer, runner, dad, dude & occasional grumpy-pants. Bother me on Twitter & GitHub if you feel like it.