milankinen / react-combinators

[NOT MAINTAINED] Seamless combination of React and reactive programming
MIT License
93 stars 7 forks source link

Support for Stateless Components #6

Closed mingfang closed 8 years ago

mingfang commented 8 years ago

React recently introduced the Stateless Components http://facebook.github.io/react/blog/2015/09/10/react-v0.14-rc1.html#stateless-function-components.

By looking at the createComponent() code, it seems that React-Combinators currently creates "Stateful" Components.

Do you think it's worthwhile to introduce a way to create Stateless Components, e.g. createStateComponent()?

milankinen commented 8 years ago

You can use combinators with stateless components whenever you want. :) However, with combinators you don't need to use components at all. You can just render you UI by using normal function calls (like this) and that doesn't cause you any performance penalties because vdom is created only once.

But yes, you can use normal stateless React components with combinators. Just remember that then you can't store local state (by using observables) into element:

import React from "react"
import Bacon from "baconjs"
import {render} from "react-dom"
import {Combinator} from "react-combinators/baconjs"

// editor models
const counter = (initial) => {
  const inc = createAction()
  const dec = createAction()
  const value = inc.$.map(1).merge(dec.$.map(-1)).scan(initial, (state, step) => state + step)
  return { value, inc, dec }
}
const name = (initialFirst, initialLast) => {
  const setFirst = createAction()
  const setLast = createAction()
  const first = setFirst.$.toProperty(initialFirst)
  const last = setLast.$.toProperty(initialLast)
  return { first, last, setFirst, setLast }
}

// editor components
const CounterEditor = ({model: {value, inc, dec}}) => {
  return (
    <Combinator>
      <div className="counter">
        <span className="val">{value}</span>
        <button className="inc" onClick={inc}>+</button>
        <button className="dec" onClick={dec}>-</button>
      </div>
    </Combinator>
  )
}
const NameEditor = ({model: {first, last, setFirst, setLast}}) => {
  return (
    <Combinator>
      <div>
        First name: <input value={first} onChange={e => setFirst(e.target.value)} /> <br />
        Last name: <input value={last} onChange={e => setLast(e.target.value)} />
      </div>
    </Combinator>
  )
}

const EDITORS = {
  counter: {create: () => counter(0), Component: CounterEditor},
  name: {create: () => name("Foo", "Bar"), Component: NameEditor}
}

const EditorSelection = ({addEditor, modifySelection}) => {
  return (
    <div>
      Add new editor
      <select onChange={e => modifySelection(e.target.value)}>
        <option value="counter">Counter</option>
        <option value="name">Name</option>
      </select>
      <button id="add" onClick={addEditor}>Add</button>
    </div>
  )
}

const App = () => {
  const modifySelection = createAction()
  const addEditor = createAction()

  const selection =
    modifySelection.$
      .map(id => EDITORS[id])
      .startWith({create: () => counter(0), Component: CounterEditor})

  const editors =
    selection.sampledBy(addEditor.$)
      .scan([], (editors, {create, Component}) => [...editors, {model: create(), Component}])

  const numEditors =
    editors.map(".length")

  return (
    <Combinator>
      <div>
        <h1>Editors {numEditors}</h1>
        <EditorSelection modifySelection={modifySelection} addEditor={addEditor} />
        <div>
          {editors.map(editors => editors.map(({Component, model}) => (
            <Component model={model} />
          )))}
        </div>
      </div>
    </Combinator>
  )
}

function createAction() {
  const bus = new Bacon.Bus()
  const creator = (val) => bus.push(val)
  creator.$ = bus
  return creator
}

render(<App />, document.getElementById("app"))
milankinen commented 8 years ago

If you know that you are always passing same properties to your component (e.g. by passing model that contains only observables), you can trivially create a wrapper that keeps the component state:

function keepState(Component) {
  const renderOnce = _.once(Component)
  return props => <Combinator>{Component(props)}</Combinator>
}

Now you can create your component like any other "stateless" component and also add some state into it:

const plainText = (initial = "") => {
  const setText = createAction()
  const text = setText.$.toProperty(initial)
  return { text, setText }
}

const StagedTextEditor = keepState(({model: {text, setText}}) => {
  const setStatedText =
    createAction()
  const stagedText =
    setStatedText.$.map(".target.value").toProperty("")
  const saveStaged =
    stagedText.map(text => () => { setText(text); setStatedText("") })

  return (
    <div>
      <input value={stagedText} onChange={setStatedText} />
      <button onClick={saveStaged}>Store</button>
      Stored text is: {text}
    </div>
  )
}) 

If you want to do some very complex stuff (like combine observables and normal props), then createComponent is the only way.

mingfang commented 8 years ago

@milankinen This is amazing. Thanks!