Core Abstractions

Pure Components

As in most programming paradigms, so many of the problems come down to how we manage state. The first tool in encouraging its proper curation is the usage of pure functions. The benefit of a pure function is that there’s no state. Similar to the adage “the best code is no code at all,” we make the related claim that “the best way to manage state is to have no state at all.”

With IDOM the core of your application will be built on the back of basic functions and coroutines that return VDOM models and which do so without state and without side effects. We call these kinds of model rendering functions Pure Components. For example, one might want a function which accepted a list of strings and turned it into a series of paragraph elements:

def paragraphs(list_of_text):
    return idom.html.div([idom.html.p(text) for text in list_of_text])

Stateful Components

A Stateful Component is one which uses a Life Cycle Hooks. These life cycle hooks allow you to add state to otherwise stateless functions. To create a stateful component you’ll need to apply the component() decorator to a coroutine whose body contains a hook usage. We’ll demonstrate that with a simple Click Counter:

import idom


def use_counter():
    count, set_count = idom.hooks.use_state(0)
    return count, lambda: set_count(lambda old_count: old_count + 1)


@idom.component
def ClickCount():
    count, increment_count = use_counter()
    return idom.html.button(
        {"onClick": lambda event: increment_count()},
        [f"Click count: {count}"],
    )

Component Layout

Displaying components requires you to turn them into VDOM. This transformation, known as “rendering a component”, is done by a LayoutType. Layouts are responsible for rendering components and scheduling their re-renders when they change. IDOM’s concrete Layout implementation renders Component instances into LayoutUpdate and responds to LayoutEvent objects respectively.

To create a layout, you’ll need a Component instance, that will become its root. This component won’t ever be removed from the model. Then, you’ll just need to call and await a render() which will return a JSON Patch:

with idom.Layout(ClickCount()) as layout:
    update = await layout.render()

The layout also handles the deliver of events to their handlers. Normally these are sent through a Dispatcher first, but for now we’ll do it manually. To accomplish this we need to pass a fake event with its “target” (event handler identifier), to the layout’s deliver() method, after which we can re-render and see what changed:

from idom.core.layout import LayoutEvent
from idom.testing import StaticEventHandler

static_handler = StaticEventHandler()


@idom.component
def ClickCount():
    count, increment_count = use_counter()

    # we do this in order to capture the event handler's target ID
    handler = static_handler.use(lambda event: increment_count())

    return idom.html.button({"onClick": handler}, [f"Click count: {count}"])


with idom.Layout(ClickCount()) as layout:
    update_1 = await layout.render()

    fake_event = LayoutEvent(target=static_handler.target, data=[{}])
    await layout.deliver(fake_event)

    update_2 = await layout.render()
    assert update_2.new["children"][0] == "Click count: 1"

Note

Don’t worry about the format of the layout event’s target. Its an internal detail of the layout’s implementation that is neither necessary to understanding how things work, nor is it part of the interface clients should rely on.

Layout Dispatcher

A “dispatcher” implementation is a relatively thin layer of logic around a Layout which drives the triggering of events and updates by scheduling an asynchronous loop that will run forever - effectively animating the model. The simplest dispatcher is dispatch_single_view() which accepts three arguments. The first is a Layout, the second is a “send” callback to which the dispatcher passes updates, and the third is a “receive” callback that’s called by the dispatcher to collect events it should execute.

import asyncio

from idom.core.layout import LayoutEvent
from idom.core.dispatcher import dispatch_single_view


sent_patches = []

# We need this to simulate a scenario in which events arriving *after* each update
# has been sent to the client. Otherwise the events would all arrive at once and we
# would observe one large update rather than many discrete updates.
semaphore = asyncio.Semaphore(0)


async def send(patch):
    sent_patches.append(patch)
    semaphore.release()
    if len(sent_patches) == 5:
        # if we didn't cancel the dispatcher would continue forever
        raise asyncio.CancelledError()


async def recv():
    await semaphore.acquire()
    event = LayoutEvent(target=static_handler.target, data=[{}])
    return event


await dispatch_single_view(idom.Layout(ClickCount()), send, recv)
assert len(sent_patches) == 5

Note

The create_shared_view_dispatcher(), while more complex in its usage, allows multiple clients to share one synchronized view.

Layout Server

The Dispatcher allows you to animate the layout, but we still need to get the models on the screen. One of the last steps in that journey is to send them over the wire. To do that you need a ServerFactory implementation. Presently, IDOM comes with support for the following web servers:

However, in principle, the base server class is capable of working with any other async enabled server framework. Potential candidates range from newer frameworks like vibora and starlette to aiohttp.

Note

If using or implementing a bridge between IDOM and an async server not listed here interests you, post an issue.

The main thing to understand about server implementations is that they can function in two ways - as a standalone application or as an extension to an existing application.

Standalone Server Usage

The implementation constructs a default application that’s used to serve the dispatched models:

import idom
from idom.server.sanic import PerClientStateServer

@idom.component
def View(self):
    return idom.html.h1(["Hello World"])

app = PerClientStateServer(View)
app.run("localhost", 5000)

Server Extension Usage

The implementation registers hooks into the application to serve the model once run:

import idom
from idom.server.sanic import PerClientState
from sanic import Sanic

app = Sanic()

@idom.component
def View(self):
    return idom.html.h1(["Hello World"])

per_client_state = PerClientStateServer(View, app=app)

app.run("localhost", 5000)