Build a component

Components are objects that talk to the forest.redux.Store, they come in many flavours but the most common components create bokeh widgets, connect events to actions and render themselves on state change. A component does one or all of the following tasks

  • React to state changes
  • Send actions to middleware/reducers via store.dispatch
  • Enable a pleasant user experience

The philosophy behind components is to be reactive, e.g. to be told what to represent. State management is centralised into a store such that all components have access to a joint pool of knowledge

_images/redux-component.png

A user interface component typically has a two-way connection with the Store, it can both send actions to the store to be reduced and receive state updates to render itself.

The intention is for all components to connect themselves to the store and be responsible for their own wiring. Most components contain bokeh widgets, it is recommended to have a single property component.layout that can be used to embed the component into a document.

# Inside main.py connect a component
component = Component().connect(store)

# Embed component somewhere in the document (optional)
document.add_root(component.layout)

When writing a new component a thought should be spared as to how you expect it to interact with the Store. Essentially, will it send messages to the store in a one-way fashion or does it also need to react to the latest state? Or indeed, is it a reactive component that renders itself on state update?

class ReactiveView:
    ...
    def connect(self, store):
        # Views merely react to state changes
        store.add_subscriber(self.render)

    def render(self, state):
        # Called by store.dispatch
        pass

A simple user interface component should be able to send messages to the store as and when a user interacts with it.

class OneWayUI(Observable):
    # Only sends actions to the store
    def __init__(self):
        ...
        super().__init__()  # Needed for inheritance

    def connect(self, store):
        # Add store.dispatch to list of subscribers
        self.add_subscriber(store.dispatch)

    def on_event(self):
        self.notify(action())  # Sends "message" to store.dispatch

A more sophisticated piece of UI should also be able to respond to state changes by updating its representation.

class TwoWayUI(Observable):
    # Same as OneWayUI but renders on state change
    ...
    def connect(self, store):
        # Store calls self.render with state
        store.add_subscriber(self.render)

        # Component sends actions to the store
        # when self.notify is called
        self.add_subscriber(store.dispatch)

    def on_event(self):
        self.notify(action())  # Sends "message" to store.dispatch

    def render(self, state):
        # Called by store with latest state

Centralising state management into a single entity is intended to make reasoning about the application as a whole simpler.

Warning

Care must be taken to ensure that components do not modify state in a such a way as to trigger a infinite loop

In many circumstances reacting to every state change is a wasteful, specially when intensive computation or i/o is involved. A better approach would be to create a stream of the properties of interest and only swing into action when those properties change.

from forest import rx  # minimalist functional reactive programming

class EfficientUI(Observable):
    # Only renders when a property changes

    def connect(self, store):
        ...
        stream = (rx.Stream()
            .listen_to(store)
            .map(self.to_props)
            .distinct()
        )
        stream.map(lambda props: self.render(*props))

    def to_props(self, state):
        return (state.get('prop'),)

    ...

A simple way to achieve the same effect would be to assign props to self.previous_props and to check during the render phase if self.to_props(state) is equal to self.previous_props. However, given that we’ve already embraced functional programming principles it makes sense to go the whole hog and use a stream.