0

I'm playing around with cyclejs and I'm trying to figure out what the idiomatic way to handle many sources/intents is supposed to be. I have a simple cyclejs program below in TypeScript with comments on the most relevant parts.

Are you supposed to model sources/intents as discreet events like you would in Elm or Redux, or are you supposed to be doing something a bit more clever with stream manipulation? I'm having a hard time seeing how you would avoid this event pattern when the application is large.

If this is the right way, wouldn't it just end up being a JS version of Elm with the added complexity of stream management?

import { div, DOMSource, h1, makeDOMDriver, VNode, input } from '@cycle/dom';
import { run } from '@cycle/xstream-run';
import xs, { Stream } from 'xstream';

import SearchBox, { SearchBoxProps } from './SearchBox';


export interface Sources {
    DOM: DOMSource;
}

export interface Sinks {
    DOM: Stream<VNode>
}

interface Model {
    search: string
    searchPending: {
        [s: string]: boolean
    }
}

interface SearchForUser {
    type: 'SearchForUser'
}

interface SearchBoxUpdated {
    type: 'SearchBoxUpdated',
    value: string
}


type Actions = SearchForUser | SearchBoxUpdated;

/**
 * Should I be mapping these into discreet events like this?
 */
function intent(domSource: DOMSource): Stream<Actions> {
    return xs.merge(
        domSource.select('.search-box')
            .events('input')
            .map((event: Event) => ({
                type: 'SearchBoxUpdated',
                value: ((event.target as any).value as string)
            } as SearchBoxUpdated)),

        domSource.select('.search-box')
            .events('keypress')
            .map(event => event.keyCode === 13)
            .filter(result => result === true)
            .map(e => ({ type: 'SearchForUser' } as SearchForUser))
    )
}

function model(action$: Stream<Actions>): Stream<Model> {
    const initialModel: Model = {
        search: '',
        searchPending: {}
    };

    /*
     * Should I be attempting to handle events like this?
     */
    return action$.fold((model, action) => {
        switch (action.type) {
            case 'SearchForUser':
                return model;

            case 'SearchBoxUpdated':
                return Object.assign({}, model, { search: action.value })
        }
    }, initialModel)
}



function view(model$: Stream<Model>): Stream<VNode> {
    return model$.map(model => {
        return div([
            h1('Github user search'),
            input('.search-box', { value: model.search })
        ])
    })
}

function main(sources: Sources): Sinks {

    const action$ = intent(sources.DOM);
    const state$ = model(action$);

    return {
        DOM: view(state$)
    };
}

run(main, {
    DOM: makeDOMDriver('#main-container')
});
Anthony Naddeo
  • 2,497
  • 25
  • 28

1 Answers1

2

In my opinion you shouldn't be multiplexing intent streams like you do (merging all the intent into a single stream).

Instead, you can try returning multiple streams your intent function.

Something like:

function intent(domSource: DOMSource): SearchBoxIntents {
  const input = domSource.select("...");
  const updateSearchBox$: Stream<string> = input
    .events("input")
    .map(/*...*/)

  const searchForUser$: Stream<boolean> = input
    .events("keypress")
    .filter(isEnterKey)
    .mapTo(true)

  return { updateSearchBox$, searchForUser$ };
}

You can then map those actions to reducers in the model function, merge those reducers and finally fold them

function model({ updateSearchBox$, searchForUser$ }: SearchBoxIntents): Stream<Model> {
  const updateSearchBoxReducer$ = updateSearchBox$
    .map((value: string) => model => ({ ...model, search: value }))

  // v for the moment this stream doesn't update the model, so you can ignore it
  const searchForUserReducer$ = searchForUser$
    .mapTo(model => model);

  return xs.merge(updateSearchBoxReducer$, searchForUserReducer$)
    .fold((model, reducer) => reducer(model), initialModel);
}

Multiple advantages to this solution:

  • you can type the arguments of your function and check that the right stream are passed along;
  • you don't need a huge switch if the number of actions increases;
  • you don't need actions identifiers.

In my opinion, multiplexing/demultiplexing streams is good when there is a parent/child relationship between two components. This way, the parent can only consume the events it needs to (this is more of an intuition than a general rule, it would need some more thinking :))

atomrc
  • 2,543
  • 1
  • 16
  • 20