Utilities for working with Karet


Keywords
react, kefir, reactive, observables, lenses
License
MIT
Install
npm install karet.util@0.19.2

Documentation

Karet Util · Gitter GitHub stars npm

A collection of utilities for working with Karet.

npm version Build Status Code Coverage

Contents

Reference

This library provides a large number of named exports. Typically one just imports the library as:

import * as U from 'karet.util'

About lifted functions

Many of the functions in this library are lifted so that they accept both ordinary values and observables as inputs. When such functions are given only ordinary values as inputs, they return immediately with the result value. OTOH, when such a function is given at least an observable as an input, they return an observable of results.

Debugging

U.show(...labels, any)

U.show is basically an identity function that console.logs the values flowing through. U.show works on plain values, observables, and atoms. When used with atoms, U.show also logs values written through.

For example:

const Component = ({state}) => (
  <div>
    <SubComponent subState={U.show('subState', U.view('subState', state))}/>
  </div>
)

Atoms

Creating atoms

U.atom(value)

U.atom creates a new atom with the given initial value.

For example:

const notEmpty = U.atom('initial')
notEmpty.get() // 'initial'
notEmpty.log() // [property] <value:current> initial
U.molecule([ ... ] | { ... })

U.molecule composes an atom from a template of atoms.

For example:

const xyA = U.atom({x: 1, y: 2})
const xL = U.view('x', xyA)
const yL = U.view('y', xyA)
const xyM = U.molecule({x: xL, y: yL})

When read, either as a property or via get, the atoms in the template are replaced by their values:

R.equals( xyM.get(), xyA.get() ) // true

When written to, the atoms in the template are written to with matching elements from the written value:

xyM.modify(L.set('x', 3))
xL.get() // 3
yL.get() // 2

The writes are performed holding event propagation.

It is considered an error, and the effect is unpredictable, if the written value does not match the template, aside from the positions of abstract mutables, of course, which means that write operations, set, remove, and modify, on molecules and lensed atoms created from molecules are only partial.

Also, if the template contains multiple abstract mutables that correspond to the same underlying state, then writing through the template will give unpredictable results.

U.variable()

U.variable creates a new atom without an initial value. See also U.refTo.

For example:

const empty = U.variable()
empty.get() // undefined
empty.log()
empty.set('first') // [property] <value> first

Transactions

U.holding(() => { ... })

U.holding is given a thunk to call while holding the propagation of events from changes to atoms. The thunk can get, set, remove and modify any number of atoms. After the thunk returns, persisting changes to atoms are propagated. See also U.actions and U.getProps.

For example:

const xy = U.atom({x: 1, y: 2})
const x = U.view('x', xy)
const y = U.view('y', xy)
x.log('x') // x <value:current> 1
y.log('y') // y <value:current> 2
U.holding(() => {
  xy.set({x: 2, y: 1})
  x.set(x.get() - 1)
}) // y <value> 1

Decomposing

U.destructure(atom)

U.destructure wraps a given atom or observable with a proxy that performs property access via U.view. On plain observable properties only get access is supported. On mutable atoms get, set, and deleteProperty accesses are supported.

For example,

const {name, number} = U.destructure(contact)

is equivalent to

const name = U.view('name', contact)
const number = U.view('number', contact)

Note that all property accesses through the proxy returned by U.destructure are performed via U.view. This means that the return value of U.destructure cannot be used as the atom or observable that it proxies.

Note that U.destructure is not recursive, which means that nested destructuring cannot be used. Only single level property access is proxied.

Note that U.destructure requires proper Proxy support. You need to decide whether you can use it.

U.mapElems((elemAtom, index) => any, arrayAtom)

U.mapElems performs a cached incremental map over state containing an array of values. On changes, the mapping function is only called for elements that were not in the state previously. U.mapElems can be used for rendering a list of stateless components, however, if elements in the array have unique ids, then U.mapElemsWithIds is generally preferable.

For example:

const Contacts = ({contacts}) => (
  <div>
    {U.mapElems((contact, i) => <Contact {...{key: i, contact}} />, contacts)}
  </div>
)

See the live Contacts CodeSandbox for an example.

U.mapElemsWithIds(idLens, (elemAtom, id) => any, arrayAtom)

U.mapElemsWithIds performs a cached incremental map over state containing an array of values with unique ids. On changes, the mapping function is only called for elements that were not in the state previously. U.mapElemsWithIds is particularly designed for rendering a list of potentially stateful components efficiently. See also U.mapElems.

For example:

const state = U.atom([
  {id: 1, value: 'Keep'},
  {id: 2, value: 'Calmm'},
  {id: 3, value: 'and'},
  {id: 4, value: 'React!'}
])

const Elems = ({elems}) => (
  <ul>
    {U.mapElemsWithIds(
      'id',
      (elem, id) => <li key={id}>{U.view('value', elem)}</li>,
      elems
    )}
  </ul>
)

U.mapElemsWithIds is asymptotically optimal in the sense that any change (new elements added or old elements removed, positions of elements changed, ...) has Theta(n) complexity. That is the best that can be achieved with plain arrays.

U.view(lens, atom)

U.view creates a read-write view with the given lens from the given original atom. The lens may also be an observable producing lenses. Modifications to the lensed atom are reflected in the original atom and vice verse.

For example:

const root = U.atom({x: 1})
const x = U.view('x', root)
x.set(2)
root.get() // { x: 2 }
root.set({x: 3})
x.get() // 3

One of the key ideas that makes lensed atoms work is the compositionality of partial lenses. Those equations make it possible not just to create lenses via composition (left hand sides of equations), but also to create paths of lensed atoms (right hand sides of equations). More concretely, both the c in

const b = U.view(a_to_b_PLens, a)
const c = U.view(b_to_c_PLens, b)

and in

const c = U.view([a_to_b_PLens, b_to_c_PLens], a)

can be considered equivalent thanks to the compositionality equations of lenses.

Note that, for most intents and purposes, U.view is a referentially transparent function: it does not create new mutable state—it merely creates a reference to existing mutable state.

U.view is the primary means of decomposing state for sub-components. For example:

const Contact = ({contact}) => (
  <div>
    <TextInput value={U.view('name', contact)} />
    <TextInput value={U.view('phone', contact)} />
  </div>
)

See the live Contacts CodeSandbox for an example.

Side-effects on atoms

U.set(atom, value)

U.set sets the given value to the specified atom. In case the value is actually an observable, U.set returns a sink that sets any values produced by the observable to the atom.

For example:

const Component = ({parameters}) => {
  const state = U.atom(null)
  // ...
  return (
    <div>
      {U.set(state, httpRequestAsObservable(parameters))}
      {U.ifElse(
        R.isNil(state),
        <Spinner />,
        <Editor state={state} />
      )}
    </div>
  )
}

Note that the above kind of arrangement to fetch data and set it into an atom is not needed when the data is only displayed in a read-only fashion in the UI.

Actions on atoms

U.doModify(atom, mapper)

U.doModify creates an action that invokes the modify method on the given atom with the given mapping function.

U.doRemove(atom)

U.doRemove creates an action that invokes the remove method on the given atom.

U.doSet(atom, value)

U.doSet creates an action that invokes the set method on the given atom with the given value.

Bus

Creating buses

U.bus()

U.bus() creates a new observable Bus stream. A Bus stream has the following methods:

  • bus.push(value) to explicitly emit value value,
  • bus.error(error) to explicitly emit error error, and
  • bus.end() to explicitly end the stream after which all the methods do nothing.

Note that buses and event streams, in general, are fairly rarely used with Karet. They can be useful for performing IO actions and in cases where actions from UI controls need to be throttled or combined.

See the live Counter using Event Streams CodeSandbox for an example.

U.serializer(initial[, [...actions]])

U.serializer creates a new observable property for serially executing actions, which are zero argument functions that may return a value or an observable that should eventually end. The returned property starts with the given initial value and then continues with the results of the optional array of actions. Like a Bus, the returned property also has the following extra methods:

  • bus.push(action) to explicitly push a new action to be executed,
  • bus.error(error) to explicitly emit error error, and
  • bus.end() to explicitly stop the serializer after which all the methods do nothing.

The property must be subscribed to in order to process actions.

See the Form using Context CodeSandbox for a minimalist example that uses a serializer to execute a simulated asynchronous operation.

Actions on buses

U.doEnd(bus)

U.doEnd creates an action that invokes the end method on the given bus.

U.doError(bus, error)

U.doError creates an action that invokes the error method on the given bus with the given value.

U.doPush(bus, value)

U.doPush creates an action that invokes the push method on the given bus with the given value.

Convenience

U.scope((...) => ...)

U.scope simply calls the given thunk. IOW, U.scope(fn) is equivalent to (fn)(). You can use it to create a new scope at expression level.

For example:

U.scope((x = 1, y = 2) => x + y)
// 3

U.tapPartial(action, any)

U.tapPartial is a lifted partial tap function. The given action is called for values flowing through except when the value is undefined.

For example:

U.thru(
  observable,
  ...
  U.tapPartial(value => console.log(value)),
  ...
)

U.thru(any, ...fns)

U.thru allows one to pipe a value through a sequence of functions. In other words, U.thru(x, fn_1, ..., fn_N) is roughly equivalent to fn_N( ... fn_1(x) ... ). It serves a similar purpose as the ->> macro of Clojure or the |> operator of F# and Elm, for example, or the >| operator defined in a Usenet post by some rando. See also U.through.

For example:

U.thru(1, x => x + 1, x => -x)
// -2

A common technique in JavaScript is to use method chaining: x.fn_1().fn_2(). A problem with method chaining is that it requires having objects with methods. Sometimes you may need to manipulate values that are not objects, like null and undefined, and other times you may want to use functions that are not directly provided as methods and it may not be desirable to monkey patch such methods.

U.thru is designed to work with partially applied curried and lifted functions that take the object as their last argument and can be seen as providing a flexible alternative to method chaining.

U.through(...fns)

U.through allows one to compose a function that passes its single argument through all of the given functions from left to right. In other words, U.through(fn_1, ..., fn_N)(x) is roughly equivalent to fn_N( ... fn_1(x) ... ). It serves a similar purpose as R.pipe, but has been crafted to work with lifted functions. See also U.thru.

For example:

U.through(x => x + 1, x => -x)(1)
// -2

U.toPartial(fn)

U.toPartial takes the given function and returns a curried version of the function that immediately returns undefined if any of the arguments passed is undefined and otherwise calls the given function with arguments.

For example:

U.toPartial((x, y) => x + y)(1, undefined)
// undefined
U.toPartial((x, y) => x + y)(1, 2)
// 3

React helpers

Binding

U.getProps({...propName: atom|bus})

U.getProps returns an event callback that gets the values of the properties named in the given template from the event target and pushes or sets them to the buses or atoms that are the values of the properties. In case the template contains multiple properties, the results are written while holding change propagation.

For example:

const TextInput = ({value}) => (
  <input type="text" value={value} onChange={U.getProps({value})} />
)
const Checkbox = ({checked}) => (
  <input type="checkbox" checked={checked} onChange={U.getProps({checked})} />
)
U.setProps({...propName: observable})

U.setProps returns a callback designed to be used with ref that subscribes to observables in the given template and copies values from the observables to the named properties in the DOM element. This allows one to bind to DOM properties such as scroll position that are not HTML attributes. See also U.actions.

See the live Scroll CodeSandbox for an example.

Input components

<U.Input {...{value|checked}} />

U.Input is a wrapper for an input element that binds either onChange={U.getProps({value})} or onChange={U.getProps({checked})} when either value or checked is a defined property.

For example:

const checked = U.atom(false)
// ...
<U.Input type="checkbox" checked={checked} />
<U.Select {...{value}} />

U.Select is a wrapper for a select element that binds onChange={U.getProps({value})} when value is a defined property.

For example:

const value = U.atom('')
// ...
<U.Select value={value} />
<U.TextArea {...{value}} />

U.TextArea is a wrapper for a textarea element that binds onChange={U.getProps({value})} when value is a defined property.

For example:

const value = U.atom('')
// ...
<U.TextArea value={value} />

Refs

U.refTo(variable)

U.refTo is designed for getting a reference to the DOM element of a component. See also U.variable. See also U.actions.

For example:

const Component = ({dom = U.variable()}) => (
  <div ref={U.refTo(dom)}>
    ...
  </div>
)

React calls the ref callback with the DOM element on mount and with null on unmount. However, U.refTo does not write null to the variable. The upside of skipping null and using an initially empty variable rather than an atom is that once the variable emits a value, you can be sure that it refers to a DOM element.

Note that in case you also want to observe null values, you can use U.set instead of U.refTo:

const Component = ({dom = U.variable()}) => (
  <div ref={U.set(dom)}>
    ...
  </div>
)

Lifecycle

U.onUnmount(action)

U.onUnmount allows you to perform an action when a component is unmounted.

For example:

const Component = () => {
  console.log('Mounted!')
  return (
    <div>
      {U.onUnmount(() => console.log('Unmounted!'))}
    </div>
  )
}

Events

U.actions(...actions)

U.actions is designed for creating an action from multiple actions. It returns an unary action function that calls the functions in the arguments with the same argument. In case there are multiple actions, they are performed while holding change propagation.

For example:

const InputValue = ({value, onChange}) => (
  <input value={value} onChange={U.actions(onChange, U.getProps({value}))} />
)

Note that in case onChange is not given to the above component as a property it causes no problem as U.actions does not attempt to call undefined.

Note that U.actions can also be used with actions given to the React ref property.

U.preventDefault(event)

U.preventDefault invokes the preventDefault method on the given object.

U.stopPropagation(event)

U.stopPropagation invokes the stopPropagation method on the given object.

Class names

U.cns(...classNames)

U.cns is designed for creating a list of class names for the className property. It joins the truthy values from the arguments with a space. In case the result would be an empty string, undefined is returned instead.

For example:

const Component = ({className}) => (
  <div className={U.cns(className, 'a-class-name', false, 'another-one', undefined)} />
)

Interop

U.pure(Component)

U.pure wraps the given component inside a PureComponent. U.pure can be used for preventing unnecessary rerenders when a Karet component is used as a child of a React component that rerenders its children even when their props do not change. See also U.toReact.

U.toKaret(ReactComponent)

U.toKaret converts a React component that takes plain value properties to a Karet component that can be given observable properties. U.toKaret is useful when using React components, such as React Select, as children of Karet components and with observable rather than plain value properties. U.toKaret is a synonym for fromClass from the Karet library.

U.toReact(KaretComponent)

U.toReact converts a Karet component that expects observable properties and should not be rerendered unnecessarily into a React component that takes plain value properties. U.toReact may be needed particularly when a Karet component is controlled by a higher-order React component, such as React Router, because Karet components typically (are not and) must not be rerendered unnecessarily. U.toReact is equivalent to U.toReactExcept(() => false). See also U.pure.

U.toReactExcept(propName => boolean, KaretComponent)

U.toReactExcept converts a Karet component that expects observable properties and should not be rerendered unnecessarily into a React component that takes plain value properties. The given predicate is used to determine which properties must not be converted to observable properties. Like U.toReact, U.toReactExcept may be needed particularly when a Karet component is controlled by a higher-order React component, such as React Router, because Karet components typically (are not and) must not be rerendered unnecessarily. See the Calmm function to React class CodeSandbox for an example.

Kefir

Algebras

U.IdentityLatest

U.IdentityLatest is a Static Land compatible algebra module over properties, like U.Latest, or plain values.

U.Latest

U.Latest is a Static Land compatible algebra module over properties from which only the latest value is propagated. Currently it implements the monad algebra.

For example:

log(
  L.traverse(
    U.Latest,
    x => U.startWith(undefined, U.later(x*1000, x)),
    L.elems,
    [1, 2, 3]
  )
)

Conditionals

U.and(...any)

U.and is a lazy variadic logical and over potentially observable properties. U.and(l, r) does not subscribe to r unless l is truthy.

U.cond(...[condition, consequent][, [alternative]])

U.cond allows one to express a sequence of conditionals. U.cond translates to a nested expression of U.ifElses.

U.cond( [ condition, consequent ]
      , ...
    [ , [ alternative ] ] )

The last [ alternative ], which, when present, needs to be a singleton array, is optional.

U.ifElse(condition, consequent, alternative)

U.ifElse is basically an implementation of the conditional operator condition ? consequent : alternative for observable properties.

U.ifElse(condition, consequent, alternative) is roughly shorthand for

U.toProperty(
  U.flatMapLatest(boolean => (boolean ? consequent : alternative), condition)
)

except that the consequent and alternative expressions are only evaluated once.

U.not(any)

U.not is a logical negation over a potentially observable property.

U.or(...any)

U.or is a lazy variadic logical or over potentially observable properties. U.or(l, r) does not subscribe to r unless l is falsy.

U.unless(condition, alternative)

U.unless(condition, alternative) is shorthand for U.ifElse(condition, undefined, alternative).

U.when(condition, consequent)

U.when(condition, consequent) is shorthand for U.ifElse(condition, consequent, undefined).

Animation

U.animationSpan(milliseconds)

U.animationSpan creates a property of increasing values from 0 to 1 for the given duration in milliseconds on each animation frame as generated by requestAnimationFrame.

See the live Animation CodeSandbox for an example.

Lifting

U.combine([...any], fn)

U.combine is a special purpose Kefir observable property combinator designed for combining properties for a sink that accepts both observable properties and constant values such as the Reactive VDOM of Karet.

Unlike typical property combinators, when U.combine is invoked with only constants (no properties), then the result is computed immediately and returned as a plain value. This optimization eliminates redundant properties.

The basic semantics of U.combine can be described as

U.combine(xs, fn) === Kefir.combine(xs, fn).skipDuplicates(R.identical)

where Kefir.combine and skipDuplicates come from Kefir and R.identical from Ramda. Duplicates are skipped, because that can reduce unnecessary updates. Ramda's R.identical provides a semantics of equality that works well within the context of embedding properties to VDOM.

Unlike with Kefir.combine, any of the argument xs given to U.combine is allowed to be

  • a constant,
  • a property, or
  • a data structure of nested arrays and plain objects containing properties.

In other words, U.combine also provides functionality similar to Bacon.combineTemplate.

Note: U.combine is carefully optimized for space—if you write equivalent combinations using Kefir's own operators, they will likely take more memory.

U.lift((...) => ...)

U.lift allows one to lift a function operating on plain values to a function that operates both on plain values and on observable properties. When given only plain values, the resulting function returns a plain value. When given observable properties, the resulting function returns an observable property of results. See also U.liftRec

For example:

const includes = U.lift( (xs, x) => xs.includes(x) )

const obsOfBooleans = includes(obsOfArrays, obsOfValues)

U.lift works well for simple functions that do not return functions. If you need to lift higher-order functions that return new functions that should also be lifted, try U.liftRec.

U.liftRec((...) => ...)

U.liftRec allows one to lift a function operating on plain values to a function that operates both on plain values and on observable properties. When given only plain values, the resulting function returns a plain value. When given observable properties, the resulting function returns an observable property of results. See also U.lift.

For example:

const either = U.liftRec(R.either)
const equals = U.lift(R.equals)

const obsOfBooleans = either(R.equals(obs1), R.equals(obs2))

U.liftRec is designed to be very simple to use. For example, the Kefir Ramda library simply wraps every Ramda function with liftRec and this results in a library that has essentially the same signature (currying and variable argument functions work the same) as Ramda except that the functions also work on Kefir observables.

Curried combinators

Kefir is a traditional JavaScript library that provides a fluent API using method chaining. Karet Util supports more functional style JavaScript by providing curried functions for programming with Kefir. The functions provided by Karet Util also try to avoid constructing observables unnecessarily.

The following are simply links to the relevant Kefir documentation:

Additional combinators

U.consume(action, observable)

U.consume creates a property that simply immediately produces undefined and subscribes to the given observable whose values it passes to the given action for as long as the returned property is subscribed to. U.consume can be used for executing side-effects during the time that a component is mounted. See also U.sink.

For example:

const DocumentTitle = ({title}) => (
  <React.Fragment>
    {U.consume(title => {
      if (typeof document !== 'undefined') document.title = title
    }, title)}
  </React.Fragment>
)
U.endWith(value, observable)

U.endWith creates an observable that ends with the given value. That is, after the given observable ends, the given value is emitted.

U.lazy(() => observable)

U.lazy allows to create an observable lazily.

For example, one use case for U.lazy is to create cyclic observables:

const sequence = ['', '', '', '', '', '']
const loop = () =>
  U.serially([U.serially(sequence.map(U.later(100))), U.lazy(loop)])

See the live Login CodeSandbox for an example.

U.fromPromise(() => promise | {ready, abort})

U.fromPromise converts a thunk that returns a promise or an object of the shape {ready, abort} where ready is a promise and abort is an action that aborts the promise into a Kefir property. The thunk is invoked once when the property is subscribed to for the first time. If an abort action is defined and all subscriptions of the property are closed before the promise resolves, the property is ended and the abort action is called once.

For example:

const fetchJSON =
  typeof AbortController === 'undefined'
    ? (url, params = {}) =>
        U.fromPromise(() => fetch(url, params).then(res => res.json()))
    : (url, params = {}) =>
        U.fromPromise(() => {
          const controller = new AbortController()
          return {
            ready: fetch(url, {...params, signal: controller.signal}).then(
              res => res.json()
            ),
            abort() {
              controller.abort()
            }
          }
        })

See the live GitHub search CodeSandbox for an example.

Note that U.fromPromise is not the same as Kefir's fromPromise.

U.sink(observable)

U.sink creates a property that simply immediately produces undefined and subscribes to the given observable for as long as the returned sink is subscribed to. See also U.consume.

U.skipIdenticals(observable)

U.skipIdenticals is shorthand for U.skipDuplicates(Object.is).

U.skipWhen(predicate, observable)

U.skipWhen(predicate) is shorthand for U.skipUnless(x => !predicate(x)).

U.startWith(value, observable)

U.startWith creates a property that starts with the given value. It uses the toProperty method of Kefir.

U.template([ ... ] | { ... })

U.template composes an observable property from an arbitrarily nested template of objects and arrays observables.

For example:

const abProperty = U.template({a: anObservable, b: anotherObservable})
abProperty instanceof Kefir.Observable // true

Subscribing

U.on({value, error, end}, observable)

U.on subscribes to an observable and dispatches the events to the actions specified in the template.

Note that explicitly subscribing to an observable should be done very rarely in a Karet application! Full Karet applications can be written with zero uses of explicit observable subscriptions, because the Reactive VDOM of Karet automatically subscribes to and unsubscribes from observables. Nevertheless, it can sometimes be convenient to subscribe explicitly to observables to perform side-effects.

For example:

U.thru(
  observable,
  ...,
  U.on({
    value: value = console.log(value)
  })
)

Lifted standard functions

Standard JavaScript functions only operate on plain values. Karet Util provides lifted versions of some useful standard JavaScript functions. The below just directly links to relevant documentation in MDN.

JSON

URIs

Math

String