Calmm Training

https://rekotiira.github.io/calmm-training/

Reko Tiira

Agenda

  • Introduction to Calmm
  • Hands-on excercises
  • How to start using Calmm today
  • Benefits of Calmm
  • Painpoints with Calmm

What is Calmm?

  • An architecture and a collection of libraries
  • Declarative and boilerplate-free way to write reactive UIs
  • Some of the libraries are React specific but not all
  • State is observable. Dependent computations are observable. You can use either Bacon or Kefir.

How is it different from traditional React?

Observables are embedded directly in the VDOM


  import * as React from 'baret' // or karet for Kefir

  const MyComponent = () => {
    const time = Bacon.once()
      .merge(Bacon.interval(1000))
      .map(() => new Date().toString())
      .toProperty()

    return <div>The time is <strong>{time}</strong></div>
  }
            

karet and baret hooks into the createElement API of React, which is the reason you need to import React from karet or baret library.

Embedding observables directly into VDOM has a lot of benefits

Completely boilerplate-free

  • State is stored in modifiable and observable atoms (more about this in a minute)
  • No glue code needed to transform external state into props or internal React component state
  • No need for
    
    Bacon.combineTemplate({
      todos: todosProperty,
      counter: counterProperty
    })
      .takeUntil(componentUnmountStream)
      .onValue(state => this.setState(state))
                      
  • ... or ...
    
    const mapStateToProps = (state, ownProps) => ({
      todos: state.todos,
      counter: state.counter
    })
    const mapDispatchToProps = ...
    const WrappedComponent = connect(mapStateToProps, mapDispatchToProps)(TodoComponent)
                      
  • ... etc.

Observable life-cycle is automatic

  • Calmm subscribes to your observables when a component is mounted
  • ... and unsubscribes when unmounted
  • ... but only when the VDOM path is actually being rendered:
    
    const UselessComponent = ({ observable }) =>
      <div>
        {new Date().getFullYear() >= YEAR_OF_LINUX_DESKTOP
          ? observable
          : 'The year of the desktop linux desktop is not yet here'}
      </div>
                      

    observable is only subscribed to if the year of linux desktop has been reached.

  • Enables you to lazily process dependent computations, which is very useful for expensive computations, or computations that may not need to be processed at all.
  • ... and you get this without writing a single line of extra code.

When an observable changes only the relevant part of the VDOM is updated


  const MyComponent = ({ observable }) =>
    <div>
      <ComplexComponent />
      The value of observable is: <strong>{observable}</strong>
    </div>
              
  • When observable changes, only the strong element is re-rendered
  • Fast incremental VDOM updates
  • No need for shouldComponentUpdate
  • Eliminates need for render optimization in vast majority of cases
  • There are exceptions though

You can write your components using functional React components

  • No need for createClass or React.Component
  • The function is only called once, on mount
  • Concise components, with a lot of the React boilerplate removed

Component properties can either be values or observables

Example: working with values and observables


const MultiplyByTwo = ({ number }) => <div>{number * 2}</div>
              

Does not work with observables.


const MultiplyByTwo = ({ number }) => <div>{number.map(it => it * 2)}</div>
              

Does not work with plain values.


import * as U from 'karet.util'
import * as R from 'kefir.ramda'

const MultiplyByTwoA = ({ number }) => <div>{U.mapValue(it => it * 2, number)}</div>
const MultiplyByTwoB = ({ number }) => <div>{R.multiply(number, 2)}</div>
              

Works with both!

U.mapValue and R.multiply are lifted to work with both plain values and observables.

Lifted functions

  • If you have a function that works with plain values, lifting it means that the lifted version can accept plain values and observables as arguments.
  • When a lifted function is called with only plain values as arguments, the result is computed immediately and is also a plain value.
  • When any of the arguments is an observable, the result is an observable as well.
  • karet.util (and baret.util) contain a lot of useful utility functions, many of which are lifted.
  • Calmm also has a few wrapper libraries that lift the functions of the wrapped libraries, i.e. kefir.ramda and kefir.partial.lenses.
  • It is easy to lift your own functions to work with observables:
    
    import * as U from 'karet.util'
    
    const includes = U.lift((xs, x) => xs.includes(x))
    const obsOfBooleans = includes(obsOfArrays, obsOfValues)
                      

Examples of lifted utility functions in calmm


const PositiveAndLessThan100 = ({ number }) =>
  <div>
    Is {number} positive and less than 100:
    {U.ifElse(
      U.and(R.gte(number, 0), R.lt(number, 100)),
      'yes',
      'no')}
  </div>
              

Also possible to write


const PositiveAndLessThan100 = ({ number }) => {
  const test = U.lift(number => number >= 0 && number < 100)
  return (
    <div>
      Is {number} positive and less than 100: {U.ifElse(test(number), 'yes', 'no')}
    </div>
  )
}
              

or


const PositiveAndLessThan100 = ({ number }) =>
  <div>
    Is {number} positive and less than 100:
    {U.mapValue(number => (number >= 0 && number < 100) ? 'yes' : 'no', number)}
  </div>
              

If you have complex conditions it is sometimes useful to break down the logic into a separate lifted function for readability.

Examples of lifted utility functions in calmm

  • What you saw in the previous examples were examples of dependent computations.
  • When the inputs change, so do the outputs. The output can be a plain value, instance of a component, etc.
  • When the observables change, the dependent computations are re-run, keeping your UI consistent with your application state.
  • Even complex and asynchronous computations are quite trivial compared to many other tools.

Example of an asynchronous dependent computation


const Search = ({ text = U.atom('') }) => {
  const getSearchUrl = text => `https://my.api/search?text=${encodeURIComponent(text)}`

  const results = U.thru(
    text,
    U.debounce(300),
    U.flatMapLatest(text => U.thru(
      U.fromPromise(() => fetch(getSearchUrl(text))),
      U.startWith(undefined)
    )),
    U.startWith([])
  )

  return (
    <div>
      <U.Input value={text} />
      {U.ifElse(
        R.is(Array, results),
        <ul>
          {U.mapElems((result, i) => <li key={i}>{result}</li>, results)}
        </ul>,
        'Loading'
      )}
    </div>
  )
}
              

Atoms

  • Used to store state
  • Atoms are observable (properties)
  • Atoms are modifiable (modify / set)
  • Stored state is immutable data
  • Can be decomposed into smaller slices of state using lenses

const Counter = ({ count = Atom(0) }) =>
  <span>
    <button onClick={() => count.modify(R.add(-1))}>-</button>
    {count}
    <button onClick={() => count.modify(R.add(1))}>+</button>
  </span>
            
Having the count atom as a parameter makes it easy to use the component with local or global (external) state, making the component more composeable and flexible.

<Counter />
            

const count = Atom(10)
<Counter count={count} />
            

Lenses & lensed atoms

  • Lensed atom is a subset of an atom that is decomposed with a lens
  • Lensed atoms are observable (properties)
  • Lensed atoms are modifiable (modify / set)
  • Can be decomposed into even smaller slices using lenses
  • Easily build a data structure that follows the structure of your UI

Example: decompose state to child components


const counters = Atom([0, 0, 0])

<Counter count={counters.view(0)} />
<Counter count={counters.view(1)} />
<Counter count={counters.view(2)} />
              

Example: filter a list to only show some elements


const todos = Atom([
  { title: 'Wash laundry', finished: false },
  { title: 'Buy groceries', finished: true },
  { title: 'Clean kitchen', finished: false }
])

const TodoList = ({ todos }) =>
  <div>
    <h2>Todo</h2>
    {U.mapElems(
      (todo, i) => <TodoItem key={i} todo={todo} />,
      todos
    )}
  </div>

const UnfinishedTodos = ({ todos }) => {
  const onlyUnfinishedLens = L.filter(todo => !todo.finished)
  return <TodoList todos={todos.view(onlyUnfinishedLens)} />
}
              

Example: change a value in an object


const obj = Atom({
  foo: {
    bar: [1, 2, 3]
  }
})

// focus the 2nd element of the foo.bar array
const slice = obj.view(['foo', 'bar', 1])

slice.set(20)
obj.get() // { foo: { bar: [ 1, 20, 3 ] }}

slice.modify(R.multiply(2))
obj.get() // { foo: { bar: [ 1, 40, 3 ] }}
              

Example: append a value to a list


const list = Atom({ items: ['x', 'y'] })
list.modify(L.set(['items', L.append], 'z'))
list.get() // { items: ['x', 'y', 'z'] }
              
Without the L.append

const list = Atom({ items: ['x', 'y'] })
list.modify(L.set(['items'], 'z'))
list.get() // { items: 'z' }
                
For simple appends Ramda's R.append is usually simpler though

const list = Atom(['x', 'y'])
list.modify(R.append('z')) // list.modify(L.set(L.append, 'z'))
list.get() // ['x', 'y', 'z']
                

Example: remove a value from a list


const list = Atom(['x', 'y', 'z'])
list.modify(L.remove(1))
list.get() // ['x', 'z']
              

Lensed atoms can also remove themselves


const list = Atom(['x', 'y', 'z'])
const item = list.view(1)
item.remove()
list.get() // ['x', 'z']
              

This is a very useful pattern when you want to have removable UI components where the remove button is in the removable component itself.

Lenses/optics are very powerful, this is just scratching the surface

  • Fetch data from API and transform into format that UI expects.
  • Make changes via UI, lenses are bidirectional so you can instantly PUT changes back to API. Just define the format of your data via lenses once.
  • Define default & required values.
  • Not just lenses. Traversals and isomorphisms.

Calmm libraries we will be using today

  • karet - embedding Kefir observables in React VDOM
  • kefir.atom - managing the state
  • karet.util - utilities for karet; today we are mainly interested in U.mapElems for easily iterating over an array of values in atom
  • partial.lenses - decomposing atoms with lenses

Excercise: a simple contact book React application

Open https://codesandbox.io/s/4wyz1lo5y0 in your browser and fork it.

Task 1

Re-factor the Contact component to a functional calmm-style component.

  • Define this.state.editing as an atom that is passed via an argument. You can use U.atom from karet.util library instead of having to import kefir.atom only for Atom definition.
  • Refactor the edit and done buttons to change the state of the editing atom.
  • Use U.view to decompose the contact prop into name, phone and email values; it works with plain values as well as atoms (our contact prop is a plain value for now).
  • Use U.ifElse to determine whether the contact is being edited or not.
  • After your component is no longer a class, you can remove the Component import.

Task 2

Re-factor the ContactList component so that it works even if it received contacts as an observable.

  • U.mapElems works with plain values as well. Our contacts prop is still a plain value for now.

Task 3

Create a state atom to hold the contacts and pass the contacts slice from the state atom to ContactList component.

  • Use the existing plain state object as a default value for the atom.
  • You can either use state.view(lens) or U.view(lens, state) to decompose the state atom. When using U.view you never need to worry whether the input is an atom or a plain value.
  • Show the value of the state atom in a HTML element so you can see how it changes. Add the following to the root App component in index.js:
    <pre>{U.stringify(state, null, 2)}</pre>

Task 4

Make the contacts actually editable.

  • By now you have decomposed state all the way down to the Contact component. Implement onChange handler for the text input to update the corresponding atom's value.
  • Alternatively you can use a convenient utility component U.Input.

Task 5

Make the contacts deletable.

  • Atoms (and LensedAtoms) can easily be removed with atom.remove.

Task 6

Make it possible to add new contacts.

  • Append an object with empty string (or any default value you want) for name, email and phone at the end of the contacts array in the state atom.

Task 7

Display the total contact count somewhere.

What was the first solution you came up with?

Task 8

Add "editing" class to the Contact component root element when editing.

  • Tip: U.cns is an useful utility function for this.
  • Tip: U.cns is lifted, so you can give observables as inputs. U.when is quite useful also.

Task 9

Only allow one contact to be edited at one time.

  • Tip: Persist the editing state of a contact in the contacts atom instead of component local state.
  • Tip: In the ContactList component create a derived computation which searches for a contact where the editing is set to true.

Bonus tasks

  1. Add a search text input and filter the visible contacts by it. Persist the search text value in the root atom. L.filter will be useful.
  2. Persist the root contact atom in local storage using the atom.storage library.
  3. Add validation to the contact editing. Validate the email and phone number using the partial.lenses.validation library.

How to start using Calmm today

  • You don't have to use everything
  • Join #calmm in Slack and calmm-js gitter for help
  • Easy to interop with existing apps, let's look at some examples

Example: Passing observables to non-calmm components


import * as React from 'karet'
import { Map } from '3rd-party-map'

<Map position={currentPositionAtom} />
              

React will throw an error if you try to pass an observable to the Map component, since it doesn't use karet/baret.

Easy way to fix this is using karet-lift attribute. If the Map component is used a lot, you can also create a lifted version of the component and use that.


import * as React from 'karet'
import * as U from 'karet.util'
import { Map } from '3rd-party-map'

const LiftedMap = React.fromClass(Map)
const LiftedMap2 = U.toKaret(Map) // this does the same

<Map position={currentPositionAtom} karet-lift />
<LiftedMap position={currentPositionAtom} />
              

Now the Map component will receive the position as a plain value.

Example: Using calmm components as children of non-calmm components

Calmm components should only render when they're mounted. If you're using calmm components as children of a non-calmm component this behavior is likely to break, because the parent container is being re-rendered which also causes the calmm components to re-render.

karet.util library has an useful utility function U.toReact which converts plain value props into observable props and stops re-rendering of the calmm component.


import * as U from 'karet.util'
import CalmmComponent from './components/calmm-component'

const CalmmComponentInterop = U.toReact(CalmmComponent)

class ReactComponent extends React.Component {
  render() {
    return <CalmmComponentInterop {...this.props} />
  }
}
              

Especially useful with libraries such as react-router.

Why is Calmm so good?

  • The least amount of boilerplate or glue code of any UI framework/library/architecture I've ever used. By far.
  • Super flexible state management with atoms and optics.
  • Decomposition of state allows for real plug-and-play components; no need for wiring to any stores or writing actions to update state
  • The same state concept used for local and global state. Easy to move state around. Easy to refactor components.
  • Consistent dependent computations and data immutability eliminates a lot of potential bugs.
  • Asynchronous problems are that much easier to solve than with other tools.
  • Quite easily testable UI code. It is easy to validate changes to atoms passed to components. Separating optics into their own meta models make them also easily testable.
  • After you get used to the new way of writing components, it is actually very clean and easy to read and understand compared to i.e. class components.
  • Easy to write efficient UIs due to minimal, incremental VDOM updates.

Painpoints with Calmm

  • Deep(ish) learning curve
    • Need to be proficient with observables
    • Optics / lenses is a new concept for many
    • Can be difficult for non-FP programmers at first
    • The benefits are not instant or obvious to many people; hard to sell
    • BUT the things you learn are actually things that are not just specific to Calmm. They will make you a better programmer
  • Still relatively young
    • Small community, can make people wary about future of the project
    • Breaking changes in libraries; not so frequent anymore
  • It is easy to do things the "wrong way"
    • Documentation has vastly improved recently
  • Debugging issues can be a huge pain
    • Asynchronous observables in VDOM, stack traces can be very cryptic
    • Performance issues can be hard to debug
    • Lacking tooling of more popular state management libs such as Redux
  • No static type support
    • Due to the high verbosity and flexibility static typing is difficult (or even impossible) thing to do with current tools

So should I use Calmm?

Yes