Using ReasonReact with Storybook

Jan 5, 2018 22:39 · 492 words · 3 minute read reasonml react ReasonReact storybook

So today I finally decided it was a good time to try out Reason and ReasonReact, and I thought getting them working with Storybook would sure make playing with them a bit easier.

Now, those familiar with the React ecosystem would no doubt be expecting fiddling with Webpack and loaders to get this to work. But as it turns out, it is actually quite straightforward to set up Storybook alongside ReasonReact. In fact, you don’t even need to touch Webpack at all which is a welcome change. You just need to follow ReasonReact’s docs by using ReasonReact.wrapReasonForJs, which is what will give you a React component your JS can actually use:

/* src/page.re */
let page = ReasonReact.wrapReasonforJs(
    ~component,
    (jsProps) => make(~message=jsProps##message, [||])
);

Now when bsb takes your .re file and generates a .bs.js file, you can just import the wrapped component from it. Reason files, and thus the generated JS files, export all their variables so importing the wrapped component is straightforward:

// stories/page.stories.js
import { page as Page } from '../src/page.bs';

Note that since variables in Reason can’t be capitalised I aliased the imported component for consistency since components typically use capitalised names, which is probably not a bad idea in general.

You could also assign the wrapped component to default in your .re file to export your component as a default export of the generated .bs.js file:

/* src/page.re */
let default = ReasonReact.wrapReasonforJs(
    ~component,
    (jsProps) => make(~message=jsProps##message, [||])
);
// stories/page.stories.js
import Page from '../src/page.bs';

The imported component can be used like any other:

// stories/page.stories.js
storiesOf('Page', module)
    .add('Moo', () => <Page message="Moo" />);

While I haven’t played around too much with Reason and ReasonReact yet, the ease of integrating it with JS so far sure makes a good first impression.


Setting it up from Scratch

You can drop Storybook into your ReasonReact project using getstorybook and it just works.

bsb -init my-project -theme react
cd my-project
getstorybook

Now just yarn start in one terminal to run bsb and yarn run storybook in another terminal to run Storybook. This means changes to your .re files immediately update your .bs.js files, and the updated JS files can be immediately reflected in your Storybook.

Also, standard caveat that these are all under constant development so while this worked at the time of writing, things may change in future.


A Full Example

/* src/page.re */

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

let handleClick = (_event, _self) => Js.log("clicked!");

let make = (~message, _children) => {
    ...component,
    render: (self) =>
        <div onClick=(self.handle(handleClick))>
            (ReasonReact.stringToElement(message))
        </div>
};

let page = ReasonReact.wrapReasonForJs(
    ~component,
    (jsProps) => make(~message=jsProps##message, [||])
);

let default = page;
// stories/page.stories.js

import React from 'react';

import { storiesOf } from '@storybook/react';

// exported using `let default = ...`
import Page from '../src/page.bs';

// exported using `let page = ...`
import { page as OtherPage } from '../src/page.bs';

storiesOf('Page', module)
    .add('Moo', () => <Page message="Moo" />);

storiesOf('OtherPage', module)
    .add('Moo2', () => <OtherPage message="Moo2" />);