Tutorial for flexx.react - reactive programming

Where classic event-driven programming is about reacting to things that happen, RP is about staying up to date with changing signals. Signals are objects that have a value which changes over time. Signals are fed with either user input or other (upstream) signal values. In that way you create a pipeline that is always kept up-to date; it defines how information flows through your application.

Introduction


In [1]:
from flexx import react

First, we create two input signals:


In [2]:
@react.input
def first_name(n='John'):
    assert isinstance(n, str)  # validation
    return n.capitalize()  # normalization

@react.input
def last_name(n='Doe'):
    assert isinstance(n, str)
    return n.capitalize()

Input signals can be called with an argument to set their value:


In [3]:
first_name()  # get signal value


Out[3]:
'John'

In [4]:
first_name('jane')  # set signal value (for input signals)
first_name()


Out[4]:
'Jane'

Now we create a new signal to react to changes in our input signals:


In [5]:
@react.connect('first_name', 'last_name')
def name(first, last):
    return '%s %s' % (first, last)

Signals produce new values, thereby transforming or combining the upstream signals. Let's create another signal to react to the "name" signal:


In [6]:
@react.connect('name')
def greet(n):
    print('hello %s!' % n)


hello Jane Doe!

So if we change either of the inputs ...


In [7]:
first_name('Guido')


hello Guido Doe!

In [8]:
last_name('van Rossum')


hello Guido Van rossum!

Observations:

  • The upstream signal (i.e. source) is specified at the callback function
  • The callback function is transformed into a signal
  • The signal produce new signal values, so you can create a stream/pipeline
  • Creating a pipeline provides a nice mechanism for caching values that take long to compute
  • Multiple upstream signals can be specified
  • It provides a nice integral way for user-provided data, as an alternative to properties or traits

The HasSignals class

Signals can also be specified at a class:


In [9]:
class Item(react.HasSignals):
    
    @react.input
    def name(n):
        return str(n)

class Collection(react.HasSignals):

    @react.input
    def items(items):
        assert all([isinstance(i, Item) for i in items])
        return tuple(list(items))
    
    @react.input
    def ref(i):
        assert isinstance(i, Item)
        return i

In [10]:
itemA, itemB, itemC, itemD = Item(name='A'), Item(name='B'), Item(name='C'), Item(name='D')
C1 = Collection(items=(itemA, itemB))
C2 = Collection(items=(itemC, itemD))

In [11]:
itemB.name()


Out[11]:
'B'

In [12]:
C1.items()


Out[12]:
(<__main__.Item at 0x7f3368230470>, <__main__.Item at 0x7f33682304a8>)

Dynamism

We subclass Collection and define reactions to signals of signals. Any change in a collections' ref or in the name of that ref will invoke an update of show_ref_name. Similary, show_index is updated when the list of items changes, or any of the names of the items:


In [13]:
class Collection2(Collection):
    
    @react.connect('ref.name')
    def show_ref_name(name):
        print('The ref is %s' % name)
    
    @react.connect('items.*.name')
    def show_index(*names):
        print('index: '+ ', '.join(names))

In [14]:
itemA, itemB, itemC, itemD = Item(name='A'), Item(name='B'), Item(name='C'), Item(name='D')
C1 = Collection2(items=(itemA, itemB))
C2 = Collection2(items=(itemC, ))


index: A, B
index: C

In [15]:
C1.ref(itemA)


The ref is A

In [16]:
C1.ref(itemD)


The ref is D

In [17]:
itemD.name('D-renamed')


The ref is D-renamed

In [18]:
C2.items([itemC, itemD])


index: C, D-renamed

In [19]:
itemC.name('C-renamed')


index: C-renamed, D-renamed

Lazy evaluation

By default react uses a push approach, which is useful in GUI's. In other situation, a pull approach might be more appropriate.


In [20]:
@react.input
def foo(v):
    return str(v)

@react.lazy('foo')
def bar(v):
    print('update bar')
    return v * 10  # imagine that this is an expensive operation

In [21]:
foo('hello')  # Does not trigger bar
foo('heya')
foo('hi')
bar()  # this is where bar gets updated


update bar
Out[21]:
'hihihihihihihihihihi'

In [22]:
bar()  # foo has not changed; cached value is returned


Out[22]:
'hihihihihihihihihihi'

Signal history

Signals store their current value as well as the previous value. (The timestamps are also stored, though these are not yet available via the public API.)


In [23]:
@react.input
def some_value(v=0):
    return float(v)
some_value(0)  # init

@react.connect('some_value')
def show_diff(s):
    print('diff: ', s - some_value.last_value)  # note: we might rename this to previous_value


diff:  0.0

In [24]:
some_value(10)


diff:  10.0

In [25]:
some_value(12)


diff:  2.0