One way you can use React from Cycle.js

  • 1
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

While Cycle.js is pretty nice for being able to isolate effects/IO in your application, you might still really like using your React-based views. Maybe you don't mind that the black-boxes that are React Components might be really dirty, as long as they (hopefully) clean up after themselves and aren't too expensive to render. Also, maybe you don't like virtual-dom for whatever reason. Either way, this post should give you one way you might use React with a Cycle.js project, or maybe spark some ideas on how to contain effects in your application.

What we're cooking today/今日のメニュー

I'm going to make an etch-a-sketch, because it's kind of fun and will demonstrate two sources of input in our system: the keyboard events and events from our React views.

Keyboard inputs, or a quick review of Cycle drivers/復習

A driver is a function that usually takes a stream of inputs from our system, and then can subscribe to that input to create the effect of pushing some output to our system. In the case of the keyboard, we don't really need anything from the application itself, so we don't need to worry about this for now.

What we do need is to be able to get the keypresses on the page, so we can do this quite simply:

const upInputs = [38, 75];
const downInputs = [40, 74];
const leftInputs = [37, 72];
const rightInputs = [39, 76];

const mappings = [
  [upInputs, 'up'],
  [downInputs, 'down'],
  [leftInputs, 'left'],
  [rightInputs, 'right'],
];

export function makeKeyboardDriver(): () => Rx.Observable<KeyboardDirection> {
  return function keyboardDriver() {
    return Rx.Observable.fromEvent(window, 'keydown')
      .map(({keyCode}) => {
        for (let i = 0; i < mappings.length; i++) {
          const [inputs, direction] = mappings[i];

          if (inputs.indexOf(keyCode) !== -1) {
            return direction;
          }
        }
      });
  }
}

It's not the prettiest looking thing ever (nor is it the best), but this will source the keydown events from window and map over the events to give us the keyCode, which we then translate to a key direction. Our driver will return an Observable of KeyboardDirection. We could clean this up further, but I'm just keeping things as simple as possible.

We have a function for preparing this user intent, passing it off to a model function to handle the key inputs to update our cursor position and draw a path and all, but that's not too important for this post. You can see the source code later yourself if you want.

Setting up and using our React driver/つくりかた

So now we have a driver for taking the keyboard direction inputs. Cool, so what about the React bit? Well, there's three ways we can communicate with our React elements, isn't there?

  1. Pass down subjects all the way down the tree (eww)
  2. Use module-imported global subjects that can be accessed from our view (eww)
  3. Use context to pass down subjects to children who opt-in to the context (hmm...)

Bonus: we could listen to real events with a top level element, but then we wouldn't get the nicely normalized React virtual events, and that would partially ruin the point of why we were using React.

I chose 3, because the other two don't sound that awesome (and might be problematic when we render this on the server and all). This makes our React driver kind of easy to write:

export function makeReactDriver<T>(
  containerId: string,
  childContextTypes: any,
  getChildContext: () => any,
  source: T
): (elements$: Rx.Observable<ReactElement>) => T {
  const container = document.getElementById(containerId);
  const ContextProvider = React.createClass({
    childContextTypes,
    getChildContext,
    render() {
      return this.props.children;
    }
  });

  return function reactDriver(element$: Rx.Observable<ReactElement>) {
    element$.subscribe(
      (element) =>
        ReactDOM.render(
          <ContextProvider>
            {element}
          </ContextProvider>,
          container
        ),
      (err) => console.error('Error occurred while rendering React', err)
    );

    return source;
  }
}

Our React driver initialization takes three args: the id of the element we will use to render our React app on, the childContextTypes that are required for identifying and passing down React context values, and the getChildContext function for actually getting the context value. With these args, we can create a ContextProvider component that will set this context and pass it down the tree.

Then our React element stream will be rendered inside the ContextProvider and they will be able to access our context appropriately. In our app, we initialize the driver this way:

const boardClear$ = new Rx.Subject();

const childContextTypes = {
  boardClear$: React.PropTypes.any
};

function getChildContext(): any {
  return {
    boardClear$
  };
};

const source = {
  boardClear$
};

let drivers = {
  keyboard: makeKeyboardDriver(),
  react: makeReactDriver('app', childContextTypes, getChildContext, source)
};

Then we can provide our view which uses the context like so:

const App = React.createClass({
  contextTypes: {
    boardClear$: React.PropTypes.any
  },

  render(): ReactElement {
    const state = this.props.state;

    return (
      <div>
        <button onClick={e => this.context.boardClear$.onNext(e)}>Clear the board</button>
        <div style={{border: '1px solid black', width: state.width, height: state.height}}>
          <Board
            state={state}
            width={state.width}
            height={state.height}
            increment={state.increment}
          />
        </div>
      </div>
    )
  }
});

export function view(state$: Rx.Observable<Props>): Rx.Observable<ReactElement> {
  return state$.map(state => <App state={state}/>);
}

This way, every time we click our button, it shoves our event object (or really, anything we want) into the subject.

Review/自画自賛

I'm tooting my own horn here, but I think this is good enough. Not the greatest, since we are hacking subjects into contexts and manually handling how we put items in our subjects, but it works all right. And of course, because we are using contexts, it is best that these subjects are static, meaning, we have a fixed number of them that we initialize in the beginning and keep it this way. In this way, it's no different than what you would do with a typical Elm project (for fun, you can s/subject/Mailbox/g this post).

Check out the code for this project here.

If you actually read this far down, please give me a toot with what you think of this approach. Is this terrible/nasty/tolerable/okay/fine/nice/good? Let me know!

Links/"情報源"